diff --git a/MAP.md b/MAP.md index 286386e..eeb4c6b 100644 --- a/MAP.md +++ b/MAP.md @@ -51,7 +51,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. (Learning/Inputs/Outputs/Settings/Help), `TrainingHealth` (real per-iteration loss curve from `nisps_ml_loss_history` + per-layer weight health from `nisps_ml_get_layer_stats`; rendered only at the Learning drawer's `expanded` depth — that IS the advanced-surface flag), `VerdictCluster` - (mode-aware), `OutputEditor`/`CurvePad`, `icons.tsx` + (mode-aware), `OutputEditor`/`DualRange`/`CurvePad`, `icons.tsx` (monochrome currentColor SVG), `model.ts` (`MF_MODES` catalogue — schema-backed modes DERIVED from `manifold/src/modes/generated/`; carries per-mode `ml` net shape + `engineId`), `output-mode.ts`. - `manifold/src/modes/generated/` — codegen output (`*_schema.ts`, do NOT hand-edit): `ModeSchema` diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index abcda42..ca5139a 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -175,7 +175,8 @@ sweep (L22, zero consumers) — don't cite them. - `icons.tsx` — monochrome inline-SVG icons (mode icons + drawer icons + `GLYPH_FALLBACK` for when monochrome is off). - `VerdictCluster.tsx` — floating bottom-centre feedback UI (perturb ▽ / undo ↺ / commit △ + A/B); labels adapt to feedback mode. - `CurvePad.tsx` — square canvas curve editor (vertical drag reshapes [0,1]; ~0.43 ≈ linear). Used in OutputEditor + OutputControlRow. -- `OutputEditor.tsx` — inline min/max/curve popup for a single output (hover/click on a bar). +- `OutputEditor.tsx` — inline range/curve popup for a single output (hover/click on a bar), using + `DualRange.tsx` for the shared dual-thumb min/max control. ### Styling — `src/styles/` CSS-variable design tokens, no CSS-in-JS. `tokens.css` `@import`s `tokens/{base,colors,fonts, diff --git a/manifold/src/console/DualRange.tsx b/manifold/src/console/DualRange.tsx new file mode 100644 index 0000000..17c3325 --- /dev/null +++ b/manifold/src/console/DualRange.tsx @@ -0,0 +1,102 @@ +/** + * DualRange — one bounded, dual-thumb control for an output's routing range. + * The same control is used by the main-view output editor and the Outputs + * drawer so min/max edits have identical interaction and clamping semantics. + */ +import { useRef } from 'react'; +import type { PointerEvent as ReactPointerEvent } from 'react'; + +export interface DualRangeProps { + min: number; + max: number; + onMin: (value: number) => void; + onMax: (value: number) => void; +} + +export function DualRange({ min, max, onMin, onMax }: DualRangeProps) { + const track = useRef(null); + const drag = useRef<{ which: 'min' | 'max' | null }>({ which: null }); + + const valueAt = (clientX: number) => { + const el = track.current; + if (!el) return 0; + const rect = el.getBoundingClientRect(); + return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); + }; + + const apply = (value: number) => { + if (drag.current.which === 'min') onMin(Math.min(value, max)); + else if (drag.current.which === 'max') onMax(Math.max(value, min)); + }; + + const down = (event: ReactPointerEvent) => { + event.currentTarget.setPointerCapture?.(event.pointerId); + const value = valueAt(event.clientX); + drag.current.which = Math.abs(value - min) <= Math.abs(value - max) ? 'min' : 'max'; + apply(value); + }; + + const move = (event: ReactPointerEvent) => { + if (drag.current.which) apply(valueAt(event.clientX)); + }; + + const up = () => { + drag.current.which = null; + }; + + return ( +
+
+ + +
+ ); +} + +function Thumb({ pct, color }: { pct: number; color: string }) { + return ( +
+ ); +} diff --git a/manifold/src/console/OutputEditor.tsx b/manifold/src/console/OutputEditor.tsx index c9f935b..7829382 100644 --- a/manifold/src/console/OutputEditor.tsx +++ b/manifold/src/console/OutputEditor.tsx @@ -5,6 +5,7 @@ import { useLayoutEffect, useRef, useState } from 'react'; import type { CSSProperties, ReactNode } from 'react'; import { CurvePad } from './CurvePad'; +import { DualRange } from './DualRange'; import type { MFParam, ParamStatus } from './model'; const OE_STATUS: { v: ParamStatus; label: string; color: string }[] = [ @@ -160,8 +161,29 @@ export function OutputEditor({ param, onChange, onHold, onLeave, place }: Output })}
- onChange({ min: v })} /> - onChange({ max: v })} /> +
+
+ + range + + + {param.min.toFixed(2)}–{param.max.toFixed(2)} + +
+ onChange({ min: v })} + onMax={(v) => onChange({ max: v })} + /> +
void; - onMax: (v: number) => void; -}) { - const track = useRef(null); - const drag = useRef<{ which: 'min' | 'max' | null }>({ which: null }); - const valAt = (clientX: number) => { - const el = track.current; - if (!el) return 0; - const r = el.getBoundingClientRect(); - return Math.max(0, Math.min(1, (clientX - r.left) / r.width)); - }; - const down = (e: ReactPointerEvent) => { - e.currentTarget.setPointerCapture?.(e.pointerId); - const v = valAt(e.clientX); - drag.current.which = Math.abs(v - min) <= Math.abs(v - max) ? 'min' : 'max'; - apply(v); - }; - const apply = (v: number) => { - if (drag.current.which === 'min') onMin(Math.min(v, max)); - else if (drag.current.which === 'max') onMax(Math.max(v, min)); - }; - const move = (e: ReactPointerEvent) => { - if (drag.current.which) apply(valAt(e.clientX)); - }; - const up = () => { - drag.current.which = null; - }; - return ( -
-
- - -
- ); -} - -function Thumb({ pct, color }: { pct: number; color: string }) { - return ( -
- ); -} - function GlyphToggle({ on, glyph,