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).
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
};
|