memlnaut-nisps/playground/src/primitives/SliderBank.tsx

100 lines
3.1 KiB
TypeScript
Raw Normal View History

feat(playground): UI primitive library + /dev/primitives showcase 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).
2026-04-29 14:38:55 +02:00
import { Component, createSignal, For, Show } from 'solid-js';
import { Slider } from './Slider';
import type { CurveName } from '../output/curves';
import styles from './SliderBank.module.css';
export interface SliderConfig {
/** Stable id used as React-style key. */
id: string;
label: string;
min: number;
max: number;
step?: number;
curve?: CurveName;
curveParam?: number;
unit?: string;
/** Optional section header to start a new collapsible group. */
section?: string;
}
export interface SliderBankProps {
sliders: ReadonlyArray<SliderConfig>;
values: () => ReadonlyArray<number>;
onChange: (id: string, v: number, index: number) => void;
/** Title rendered at the top of the bank (above all sections). */
title?: string;
/** Default-collapsed sections by name. */
collapsedSections?: ReadonlyArray<string>;
}
interface Section {
name: string;
items: Array<{ cfg: SliderConfig; index: number }>;
}
function group(configs: ReadonlyArray<SliderConfig>): Section[] {
const sections: Section[] = [];
let current: Section | null = null;
configs.forEach((cfg, index) => {
const sectionName = cfg.section ?? '';
if (!current || current.name !== sectionName) {
current = { name: sectionName, items: [] };
sections.push(current);
}
current.items.push({ cfg, index });
});
return sections;
}
export const SliderBank: Component<SliderBankProps> = (props) => {
const [collapsed, setCollapsed] = createSignal<Record<string, boolean>>(
Object.fromEntries((props.collapsedSections ?? []).map((s) => [s, true])),
);
const sections = () => group(props.sliders);
const toggle = (name: string) => {
setCollapsed((prev) => ({ ...prev, [name]: !prev[name] }));
};
return (
<div class={styles.bank}>
<Show when={props.title}>
<h3 class={styles.title}>{props.title}</h3>
</Show>
<For each={sections()}>{(section) => (
<section class={styles.section}>
<Show when={section.name}>
<button
type="button"
class={styles.sectionHeader}
onClick={() => toggle(section.name)}
aria-expanded={!collapsed()[section.name]}
>
<span class={styles.caret}>{collapsed()[section.name] ? '▸' : '▾'}</span>
<span class={styles.sectionName}>{section.name}</span>
<span class={styles.sectionCount}>{section.items.length}</span>
</button>
</Show>
<Show when={!collapsed()[section.name]}>
<div class={styles.list}>
<For each={section.items}>{({ cfg, index }) => (
<Slider
value={props.values()[index] ?? 0}
onChange={(v) => props.onChange(cfg.id, v, index)}
min={cfg.min}
max={cfg.max}
step={cfg.step}
curve={cfg.curve}
curveParam={cfg.curveParam}
label={cfg.label}
unit={cfg.unit}
/>
)}</For>
</div>
</Show>
</section>
)}</For>
</div>
);
};