Sixteen primitives at src/primitives/, each with its own .module.css and a .demo.tsx that wires the primitive to live local state. All primitives are typed (strict), pointer-event based (touch + mouse), keyboard-accessible where it makes sense, and free of business logic. Primitives: - Slider, SliderBank: value sliders with optional curve mapping; collapsible sections. - VirtualJoystick, XYPad: 2D pointer input, returning [0,1]^2. - Heatmap: NxN canvas rendering with three color modes (luminance/variance/divergence) and a configurable palette. - OutputDisplay: bar chart of N output values. - TrainingControls: thumbs-up/down/undo + train/randomise + status. - Drawer: portal'd slide-in panel from left or right; ESC + scrim close. - ControlAxis: compound-axis slider (Boldness/Memory/Precision shape) with active-preset hint, endpoint labels, double-tap-to-relink. - ProgressRing: SVG circular progress with optional inline label. - PillToggle: segmented radio control. - ParamEditor: min/max/curve/mute/pin/fixedValue editor for a single param. - JoyMap: zoom minimap with adaptive grid + zoom window, vanishing trail with Catmull-Rom spline + tap-to-return, dual concentric noise rings, region pin overlays, frozen overlay. - WeightHealth: 10-bin histogram + dead/saturating/healthy status glow. - GradientFlow: per-layer bar chart with vanishing/exploding/converged color coding. - LossPlot: log-scale line chart of training loss history. App.tsx now wires the /dev/primitives route to a lazy-loaded PrimitivesShowcase that renders all sixteen demos in a responsive grid. Lazy import keeps the home page bundle ~20 kB while the showcase ships its own ~42 kB chunk. Stream 8 of the rewrite (meml-911).
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
import { Component, createMemo } from 'solid-js';
|
|
|
|
export interface ProgressRingProps {
|
|
/** 0..1 fraction. */
|
|
progress: () => number;
|
|
/** Diameter in px. Default 32. */
|
|
size?: number;
|
|
/** Stroke color. Default --accent. */
|
|
color?: string;
|
|
/** Stroke width. Default size/10. */
|
|
strokeWidth?: number;
|
|
/** Show numeric percent in middle. */
|
|
showLabel?: boolean;
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
export const ProgressRing: Component<ProgressRingProps> = (props) => {
|
|
const size = () => props.size ?? 32;
|
|
const strokeWidth = () => props.strokeWidth ?? Math.max(2, Math.round(size() / 10));
|
|
const radius = () => (size() - strokeWidth()) / 2;
|
|
const circumference = () => 2 * Math.PI * radius();
|
|
const progress = () => Math.max(0, Math.min(1, props.progress()));
|
|
const offset = createMemo(() => circumference() * (1 - progress()));
|
|
const color = () => props.color ?? 'var(--accent)';
|
|
|
|
return (
|
|
<svg
|
|
width={size()}
|
|
height={size()}
|
|
viewBox={`0 0 ${size()} ${size()}`}
|
|
role="progressbar"
|
|
aria-valuenow={Math.round(progress() * 100)}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-label={props.ariaLabel ?? 'progress'}
|
|
>
|
|
<circle
|
|
cx={size() / 2}
|
|
cy={size() / 2}
|
|
r={radius()}
|
|
stroke="var(--bg-3)"
|
|
stroke-width={strokeWidth()}
|
|
fill="none"
|
|
/>
|
|
<circle
|
|
cx={size() / 2}
|
|
cy={size() / 2}
|
|
r={radius()}
|
|
stroke={color()}
|
|
stroke-width={strokeWidth()}
|
|
fill="none"
|
|
stroke-dasharray={`${circumference()}`}
|
|
stroke-dashoffset={`${offset()}`}
|
|
stroke-linecap="round"
|
|
transform={`rotate(-90 ${size() / 2} ${size() / 2})`}
|
|
style={{ transition: 'stroke-dashoffset 120ms linear' }}
|
|
/>
|
|
{props.showLabel && (
|
|
<text
|
|
x={size() / 2}
|
|
y={size() / 2}
|
|
dominant-baseline="central"
|
|
text-anchor="middle"
|
|
font-family="var(--font-mono)"
|
|
font-size={`${Math.round(size() / 3.5)}`}
|
|
fill="var(--fg-mute)"
|
|
>
|
|
{Math.round(progress() * 100)}
|
|
</text>
|
|
)}
|
|
</svg>
|
|
);
|
|
};
|