feat(manifold): verdict-cluster + dock polish
- thumbs-up → green; down/explore → orange (dice icon in explore mode, thumb-down in dislike mode); undo button shrunk - add nudge | randomise two-segment pill under the verdict cluster - dock drawers closed by default; collapse to two depths (condensed side-panel / expanded ~80% centred modal) with a left-edge expand tab - Mode button → 'M' (white border, orange letter) - smoke test asserts rail button title (drawers now closed by default)
This commit is contained in:
parent
453047612a
commit
cb28facc65
7 changed files with 329 additions and 172 deletions
|
|
@ -106,8 +106,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
const [tame, setTame] = useState(0.85);
|
const [tame, setTame] = useState(0.85);
|
||||||
const [health, setHealth] = useState(0.8);
|
const [health, setHealth] = useState(0.8);
|
||||||
const [rev, setRev] = useState(1);
|
const [rev, setRev] = useState(1);
|
||||||
const [active, setActive] = useState<DrawerKey | null>('learn');
|
const [active, setActive] = useState<DrawerKey | null>(null);
|
||||||
const [depth, setDepth] = useState<DrawerDepth>('peek');
|
const [depth, setDepth] = useState<DrawerDepth>('condensed');
|
||||||
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
|
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
|
||||||
const [sandwich, setSandwich] = useState(false);
|
const [sandwich, setSandwich] = useState(false);
|
||||||
|
|
||||||
|
|
@ -222,8 +222,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setFollow(false);
|
setFollow(false);
|
||||||
setPins([]);
|
setPins([]);
|
||||||
setMarkers([]);
|
setMarkers([]);
|
||||||
setActive('learn');
|
setActive(null);
|
||||||
setDepth('peek');
|
setDepth('condensed');
|
||||||
if (engine) engine.setInput(0.5, 0.5);
|
if (engine) engine.setInput(0.5, 0.5);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [modeId]);
|
}, [modeId]);
|
||||||
|
|
@ -411,6 +411,23 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setRev((r) => r + 1);
|
setRev((r) => r + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nudge — a small bounded weight perturbation of the current net (the left
|
||||||
|
* half of the secondary pill). Routes to the scratchpad while exploring,
|
||||||
|
* otherwise nudges the real net directly.
|
||||||
|
*/
|
||||||
|
const nudgeNet = () => {
|
||||||
|
const c = controllerRef.current;
|
||||||
|
if (feedbackMode === 'explore-and-place' && c?.getState().exploring) {
|
||||||
|
c.nudge();
|
||||||
|
} else {
|
||||||
|
engine?.feedback.nudge(0.05);
|
||||||
|
engine?.process();
|
||||||
|
}
|
||||||
|
syncController();
|
||||||
|
setRev((r) => r + 1);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo. While exploring (Mode 2) this pops the scratchpad undo ring (reroll /
|
* Undo. While exploring (Mode 2) this pops the scratchpad undo ring (reroll /
|
||||||
* nudge). Otherwise it falls back to the UI snapshot stack (visual A/B seed).
|
* nudge). Otherwise it falls back to the UI snapshot stack (visual A/B seed).
|
||||||
|
|
@ -526,8 +543,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
};
|
};
|
||||||
if (map[e.key]) {
|
if (map[e.key]) {
|
||||||
setActive((a) => (a === map[e.key] ? null : map[e.key]));
|
setActive((a) => (a === map[e.key] ? null : map[e.key]));
|
||||||
setDepth('peek');
|
setDepth('condensed');
|
||||||
} else if (e.key === '\\') setDepth((d) => (d === 'full' ? 'peek' : 'full'));
|
} else if (e.key === '\\') setDepth((d) => (d === 'expanded' ? 'condensed' : 'expanded'));
|
||||||
else if (focus === 'composite' && e.key === '[') {
|
else if (focus === 'composite' && e.key === '[') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSplit((s) => Math.max(0, s - 0.04));
|
setSplit((s) => Math.max(0, s - 0.04));
|
||||||
|
|
@ -903,6 +920,8 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
onUndo={undo}
|
onUndo={undo}
|
||||||
onCommit={commit}
|
onCommit={commit}
|
||||||
onReroll={reroll}
|
onReroll={reroll}
|
||||||
|
onNudge={nudgeNet}
|
||||||
|
onRandomise={reroll}
|
||||||
canUndo={feedbackMode === 'explore-and-place' && exploring ? undoDepth > 0 : snapshots.length > 0}
|
canUndo={feedbackMode === 'explore-and-place' && exploring ? undoDepth > 0 : snapshots.length > 0}
|
||||||
ab={ab}
|
ab={ab}
|
||||||
onToggleAB={toggleAB}
|
onToggleAB={toggleAB}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* The Console dock — right-edge 48px rail + one mutually-exclusive drawer with
|
* The Console dock — right-edge 48px rail + one mutually-exclusive drawer with
|
||||||
* three depth states (Peek 320 / Expand 520 / Full modal).
|
* two depth states: condensed (360px side panel) and expanded (centred modal,
|
||||||
|
* ~80% of the viewport). A vertically-centred tab on the panel's left edge
|
||||||
|
* toggles between them; drawers are closed by default.
|
||||||
*
|
*
|
||||||
* RESTRUCTURED (operator dock restructure):
|
* RESTRUCTURED (operator dock restructure):
|
||||||
* - TOP, pinned: the **Mode** selector — the active OUTPUT BACKEND/target
|
* - TOP, pinned: the **Mode** selector — the active OUTPUT BACKEND/target
|
||||||
|
|
@ -22,7 +24,6 @@ import type { ConsoleCtx, DrawerDepth, DrawerKey, OutputMode } from './types';
|
||||||
import { OUTPUT_MODES } from './output-mode';
|
import { OUTPUT_MODES } from './output-mode';
|
||||||
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
||||||
import {
|
import {
|
||||||
ModeIcon,
|
|
||||||
ParticleIcon,
|
ParticleIcon,
|
||||||
MidiIcon,
|
MidiIcon,
|
||||||
OscIcon,
|
OscIcon,
|
||||||
|
|
@ -80,17 +81,20 @@ function ModeSelector({
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 'var(--r-2)',
|
borderRadius: 'var(--r-2)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid #ffffff',
|
||||||
background: 'rgba(255,106,0,0.12)',
|
background: 'rgba(255,106,0,0.12)',
|
||||||
color: 'var(--accent)',
|
color: 'var(--accent)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: 18,
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mono ? <ModeIcon /> : '⊞'}
|
M
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -196,7 +200,7 @@ export function Dock({ ctx, active, setActive, depth, setDepth, sandwich, setSan
|
||||||
title={s.label}
|
title={s.label}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActive(on ? null : key);
|
setActive(on ? null : key);
|
||||||
setDepth('peek');
|
setDepth('condensed');
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|
@ -234,107 +238,146 @@ export function Dock({ ctx, active, setActive, depth, setDepth, sandwich, setSan
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const width = depth === 'expand' ? 520 : 320;
|
const expanded = depth === 'expanded';
|
||||||
const full = depth === 'full';
|
|
||||||
const section = active ? DRAWERS[active] : null;
|
const section = active ? DRAWERS[active] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Dim backdrop behind the centred modal; click to collapse back. */}
|
||||||
|
{section && expanded && (
|
||||||
|
<div
|
||||||
|
onClick={() => setDepth('condensed')}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 85,
|
||||||
|
background: 'rgba(0,0,0,0.45)',
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
WebkitBackdropFilter: 'blur(2px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{section && (
|
{section && (
|
||||||
|
// Shell: positions/sizes the drawer and is the anchor for the left-edge
|
||||||
|
// tab. overflow:visible so the tab (at left:-22) is NOT clipped; the
|
||||||
|
// inner wrapper handles scrolling.
|
||||||
<aside
|
<aside
|
||||||
style={
|
style={
|
||||||
full
|
expanded
|
||||||
? {
|
? {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
inset: 0,
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: '80vw',
|
||||||
|
height: '80vh',
|
||||||
zIndex: 90,
|
zIndex: 90,
|
||||||
background: 'var(--glass)',
|
background: 'var(--glass)',
|
||||||
backdropFilter: 'blur(16px)',
|
backdropFilter: 'blur(16px)',
|
||||||
WebkitBackdropFilter: 'blur(16px)',
|
WebkitBackdropFilter: 'blur(16px)',
|
||||||
padding: 'var(--sp-6)',
|
border: '1px solid var(--glass-line)',
|
||||||
display: 'flex',
|
borderRadius: 'var(--r-3)',
|
||||||
flexDirection: 'column',
|
boxShadow: 'var(--shadow-2)',
|
||||||
gap: 'var(--sp-3)',
|
overflow: 'visible',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 48,
|
right: 48,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width,
|
width: 360,
|
||||||
zIndex: 35,
|
zIndex: 35,
|
||||||
background: 'var(--glass)',
|
background: 'var(--glass)',
|
||||||
backdropFilter: 'blur(16px)',
|
backdropFilter: 'blur(16px)',
|
||||||
WebkitBackdropFilter: 'blur(16px)',
|
WebkitBackdropFilter: 'blur(16px)',
|
||||||
borderLeft: '1px solid var(--glass-line)',
|
borderLeft: '1px solid var(--glass-line)',
|
||||||
boxShadow: '-8px 0 24px rgba(0,0,0,0.4)',
|
boxShadow: '-8px 0 24px rgba(0,0,0,0.4)',
|
||||||
padding: 'var(--sp-4)',
|
overflow: 'visible',
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
overflow: 'auto',
|
|
||||||
animation: 'mfDrawerIn var(--dur-med) var(--ease-console)',
|
animation: 'mfDrawerIn var(--dur-med) var(--ease-console)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<header
|
{/* Expand/collapse tab — vertically centred on the panel's left edge. */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={expanded ? 'Condense' : 'Expand'}
|
||||||
|
onClick={() => setDepth(expanded ? 'condensed' : 'expanded')}
|
||||||
style={{
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: -22,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
width: 22,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 'var(--r-2) 0 0 var(--r-2)',
|
||||||
|
border: '1px solid var(--glass-line)',
|
||||||
|
borderRight: 'none',
|
||||||
|
background: 'var(--glass)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
WebkitBackdropFilter: 'blur(14px)',
|
||||||
|
color: 'var(--fg-mute)',
|
||||||
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 'var(--sp-2)',
|
justifyContent: 'center',
|
||||||
borderBottom: '1px solid var(--glass-line)',
|
zIndex: 1,
|
||||||
paddingBottom: 'var(--sp-2)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}>
|
<ExpandIcon size={12} />
|
||||||
{mono ? section.icon : <span style={{ fontSize: 'var(--fs-md)' }}>{section.glyph}</span>}
|
</button>
|
||||||
</span>
|
{/* Inner scroll wrapper — holds header + content, scrolls independently. */}
|
||||||
<h3 style={{ margin: 0, fontSize: 'var(--fs-md)', color: 'var(--fg)' }}>{section.label}</h3>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--fg-dim)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.1em',
|
|
||||||
marginLeft: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{depth}
|
|
||||||
</span>
|
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 4 }}>
|
|
||||||
{!full && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={depth === 'peek' ? 'More' : 'Peek'}
|
|
||||||
onClick={() => setDepth(depth === 'peek' ? 'expand' : 'peek')}
|
|
||||||
style={dockMini}
|
|
||||||
>
|
|
||||||
<ExpandIcon size={12} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Open full"
|
|
||||||
onClick={() => setDepth(full ? 'peek' : 'full')}
|
|
||||||
style={dockMini}
|
|
||||||
>
|
|
||||||
<ExpandIcon size={12} />
|
|
||||||
</button>
|
|
||||||
<button type="button" title="Close" onClick={() => setActive(null)} style={dockMini}>
|
|
||||||
<CloseIcon size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: expanded ? 'var(--sp-6)' : 'var(--sp-4)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 'var(--sp-2)',
|
gap: expanded ? 'var(--sp-3)' : 'var(--sp-2)',
|
||||||
flex: 1,
|
borderRadius: 'inherit',
|
||||||
...(full ? { maxWidth: 720 } : {}),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{section.render(ctx, depth)}
|
<header
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 'var(--sp-2)',
|
||||||
|
borderBottom: '1px solid var(--glass-line)',
|
||||||
|
paddingBottom: 'var(--sp-2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}>
|
||||||
|
{mono ? section.icon : <span style={{ fontSize: 'var(--fs-md)' }}>{section.glyph}</span>}
|
||||||
|
</span>
|
||||||
|
<h3 style={{ margin: 0, fontSize: 'var(--fs-md)', color: 'var(--fg)' }}>{section.label}</h3>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: 'var(--fg-dim)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
marginLeft: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{depth}
|
||||||
|
</span>
|
||||||
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: 4 }}>
|
||||||
|
<button type="button" title="Close" onClick={() => setActive(null)} style={dockMini}>
|
||||||
|
<CloseIcon size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 'var(--sp-2)',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{section.render(ctx, depth)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Console — the FIVE real dock drawers (operator dock restructure). Each renderer
|
* Console — the FIVE real dock drawers (operator dock restructure). Each renderer
|
||||||
* takes (ctx, depth); what shows is gated by depth (peek | expand | full):
|
* takes (ctx, depth); what shows is gated by depth (condensed | expanded):
|
||||||
*
|
*
|
||||||
* learn — Learning : feedback-mode selector, solo/arm chooser, live params
|
* learn — Learning : feedback-mode selector, solo/arm chooser, live params
|
||||||
* inputs — Inputs : input source
|
* inputs — Inputs : input source
|
||||||
|
|
@ -162,13 +162,13 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
|
|
||||||
<SectionLabel>Down action · feedback mode</SectionLabel>
|
<SectionLabel>Down action · feedback mode</SectionLabel>
|
||||||
<Segmented value={ctx.feedbackMode} onChange={ctx.setFeedbackMode} options={FEEDBACK_OPTS} />
|
<Segmented value={ctx.feedbackMode} onChange={ctx.setFeedbackMode} options={FEEDBACK_OPTS} />
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
{FEEDBACK_DESC[ctx.feedbackMode]}
|
{FEEDBACK_DESC[ctx.feedbackMode]}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel>Solo / arm scope</SectionLabel>
|
<SectionLabel>Solo / arm scope</SectionLabel>
|
||||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
|
@ -204,7 +204,7 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{depth === 'full' && (
|
{depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel>Feedback lab</SectionLabel>
|
<SectionLabel>Feedback lab</SectionLabel>
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
|
|
@ -339,21 +339,21 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
|
|
||||||
<SectionLabel>Input source</SectionLabel>
|
<SectionLabel>Input source</SectionLabel>
|
||||||
<Segmented value={inp.inputMode} onChange={inp.setInputMode} options={INPUT_MODE_OPTS} />
|
<Segmented value={inp.inputMode} onChange={inp.setInputMode} options={INPUT_MODE_OPTS} />
|
||||||
{active && depth !== 'peek' && (
|
{active && depth === 'expanded' && (
|
||||||
<span style={{ fontSize: 9, color: STATUS_TONE[active.status.state] ?? 'var(--fg-dim)' }}>
|
<span style={{ fontSize: 9, color: STATUS_TONE[active.status.state] ?? 'var(--fg-dim)' }}>
|
||||||
{active.status.message}
|
{active.status.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- Internal (XY pad / manifold) ---- */}
|
{/* ---- Internal (XY pad / manifold) ---- */}
|
||||||
{inp.inputMode === 'internal' && depth !== 'peek' && (
|
{inp.inputMode === 'internal' && depth === 'expanded' && (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
Drag the on-screen manifold / XY pad. Two axes feed the net directly — this is the default.
|
Drag the on-screen manifold / XY pad. Two axes feed the net directly — this is the default.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- Game Controller ---- */}
|
{/* ---- Game Controller ---- */}
|
||||||
{inp.inputMode === 'gamepad' && depth !== 'peek' && (
|
{inp.inputMode === 'gamepad' && depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel>Sticks</SectionLabel>
|
<SectionLabel>Sticks</SectionLabel>
|
||||||
<Segmented
|
<Segmented
|
||||||
|
|
@ -384,7 +384,7 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- MIDI ---- */}
|
{/* ---- MIDI ---- */}
|
||||||
{inp.inputMode === 'midi' && depth !== 'peek' && (
|
{inp.inputMode === 'midi' && depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel>Device</SectionLabel>
|
<SectionLabel>Device</SectionLabel>
|
||||||
{inp.midiInputs.length === 0 ? (
|
{inp.midiInputs.length === 0 ? (
|
||||||
|
|
@ -466,7 +466,7 @@ function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- Reshape note (only when >2 axes feed the fixed WASM head) ---- */}
|
{/* ---- Reshape note (only when >2 axes feed the fixed WASM head) ---- */}
|
||||||
{reshaping && depth !== 'peek' && (
|
{reshaping && depth === 'expanded' && (
|
||||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
{/* TODO(workstream F, docs/redesign/inputs-spec.md — "multiple WASM modules +
|
{/* TODO(workstream F, docs/redesign/inputs-spec.md — "multiple WASM modules +
|
||||||
warm-start"): give every axis its own genuine input dimension by (re)loading a
|
warm-start"): give every axis its own genuine input dimension by (re)loading a
|
||||||
|
|
@ -509,7 +509,7 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
|
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
|
||||||
</div>
|
</div>
|
||||||
<Slider label="master volume" value={ctx.volume} min={0} max={1} step={0.01} onChange={ctx.setVolume} />
|
<Slider label="master volume" value={ctx.volume} min={0} max={1} step={0.01} onChange={ctx.setVolume} />
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel>Tempo</SectionLabel>
|
<SectionLabel>Tempo</SectionLabel>
|
||||||
<Slider label="bpm" value={ctx.bpm} min={40} max={220} step={1} onChange={ctx.setBpm} />
|
<Slider label="bpm" value={ctx.bpm} min={40} max={220} step={1} onChange={ctx.setBpm} />
|
||||||
|
|
@ -530,7 +530,7 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
case 'particles':
|
case 'particles':
|
||||||
return depth !== 'peek' ? (
|
return depth === 'expanded' ? (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
Flow-field visualiser driven by the first {Math.min(20, ctx.params.length)} outputs (no audio).
|
Flow-field visualiser driven by the first {Math.min(20, ctx.params.length)} outputs (no audio).
|
||||||
{/* TODO(backends-spec §4): port FlowFieldVisualizer + visual preset chips. */}
|
{/* TODO(backends-spec §4): port FlowFieldVisualizer + visual preset chips. */}
|
||||||
|
|
@ -539,13 +539,13 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
case 'midi':
|
case 'midi':
|
||||||
// The full MIDI config (port picker, CC count, per-output CC/channel/name)
|
// The full MIDI config (port picker, CC count, per-output CC/channel/name)
|
||||||
// + preset bar render via OutputsBackendConfig in RoutingDrawer below.
|
// + preset bar render via OutputsBackendConfig in RoutingDrawer below.
|
||||||
return depth !== 'peek' ? (
|
return depth === 'expanded' ? (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
Each output sends a real Web MIDI CC. Pick a port and set CC# / channel per output.
|
Each output sends a real Web MIDI CC. Pick a port and set CC# / channel per output.
|
||||||
</p>
|
</p>
|
||||||
) : null;
|
) : null;
|
||||||
case 'osc':
|
case 'osc':
|
||||||
return depth !== 'peek' ? (
|
return depth === 'expanded' ? (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
Each output sends to an OSC path with a physical range, over the WebSocket bridge.
|
Each output sends to an OSC path with a physical range, over the WebSocket bridge.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -568,23 +568,8 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
const nameFor = (idx: number, fallback: string) =>
|
const nameFor = (idx: number, fallback: string) =>
|
||||||
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
|
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
|
||||||
|
|
||||||
if (depth === 'full') {
|
const expanded = depth === 'expanded';
|
||||||
return (
|
const rows = expanded ? ctx.params : ctx.params.slice(0, 6);
|
||||||
<>
|
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
||||||
<Chip tone="var(--accent)">{modeDesc.label}</Chip>
|
|
||||||
<BackendStatusChip ctx={ctx} />
|
|
||||||
</div>
|
|
||||||
{ModeConfig(ctx, depth)}
|
|
||||||
{/* Specialised, editable per-backend config (MIDI/OSC) + named-preset bar. */}
|
|
||||||
<OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />
|
|
||||||
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
|
|
||||||
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = depth === 'peek' ? ctx.params.slice(0, 6) : ctx.params;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
|
@ -596,15 +581,15 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
<BackendStatusChip ctx={ctx} />
|
<BackendStatusChip ctx={ctx} />
|
||||||
</div>
|
</div>
|
||||||
{ModeConfig(ctx, depth)}
|
{ModeConfig(ctx, depth)}
|
||||||
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden at peek. */}
|
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
|
||||||
{depth !== 'peek' && <OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />}
|
{expanded && <OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />}
|
||||||
<SectionLabel>Outputs · M mute · S arm · off/fixed/live</SectionLabel>
|
<SectionLabel>Outputs · M mute · S arm · off/fixed/live</SectionLabel>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
maxHeight: depth === 'peek' ? 220 : 460,
|
maxHeight: expanded ? 460 : 220,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -617,14 +602,20 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
param={labelled}
|
param={labelled}
|
||||||
value={values[i] ?? 0}
|
value={values[i] ?? 0}
|
||||||
onChange={(patch) => ctx.setParam(i, patch)}
|
onChange={(patch) => ctx.setParam(i, patch)}
|
||||||
showCurve={depth !== 'peek'}
|
showCurve={expanded}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{depth === 'peek' && ctx.params.length > 6 && (
|
{!expanded && ctx.params.length > 6 && (
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more — expand to edit</span>
|
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more — expand to edit</span>
|
||||||
)}
|
)}
|
||||||
|
{expanded && (
|
||||||
|
<>
|
||||||
|
<SectionLabel>Advanced · {modeDesc.label}</SectionLabel>
|
||||||
|
<BackendAdvanced backend={modeDesc.backend} params={ctx.params} setParam={ctx.setParam} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +645,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
||||||
onChange={(v) => set('monochromeIcons', v)}
|
onChange={(v) => set('monochromeIcons', v)}
|
||||||
label="Monochrome icons"
|
label="Monochrome icons"
|
||||||
/>
|
/>
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>Unfocused icon colour</div>
|
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>Unfocused icon colour</div>
|
||||||
<PillToggle
|
<PillToggle
|
||||||
|
|
@ -683,7 +674,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
||||||
onChange={(v) => set('inputMap', v as InputMapMode)}
|
onChange={(v) => set('inputMap', v as InputMapMode)}
|
||||||
options={INPUT_MAP_OPTS}
|
options={INPUT_MAP_OPTS}
|
||||||
/>
|
/>
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
The 2D input surface: a rectangular XY map or a circular joystick-style disc. "Follow mode"
|
The 2D input surface: a rectangular XY map or a circular joystick-style disc. "Follow mode"
|
||||||
uses the active mode's declared input (joystick → circular, else rectangular).
|
uses the active mode's declared input (joystick → circular, else rectangular).
|
||||||
|
|
@ -700,7 +691,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
||||||
unit="px"
|
unit="px"
|
||||||
onChange={(v) => set('cornerRadius', Math.round(v))}
|
onChange={(v) => set('cornerRadius', Math.round(v))}
|
||||||
/>
|
/>
|
||||||
{depth !== 'peek' && (
|
{depth === 'expanded' && (
|
||||||
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
Roundness of buttons, control rows, dock icons and panels. Pills and the circular verdict
|
Roundness of buttons, control rows, dock icons and panels. Pills and the circular verdict
|
||||||
buttons are intentionally exempt. Default 2px.
|
buttons are intentionally exempt. Default 2px.
|
||||||
|
|
@ -716,7 +707,7 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
|
||||||
|
|
||||||
const KEYS: [string, string][] = [
|
const KEYS: [string, string][] = [
|
||||||
['1–5', 'open drawers'],
|
['1–5', 'open drawers'],
|
||||||
['\\', 'full depth'],
|
['\\', 'expand drawer'],
|
||||||
['space / ↑', 'commit +'],
|
['space / ↑', 'commit +'],
|
||||||
['↓', 'perturb / down −'],
|
['↓', 'perturb / down −'],
|
||||||
['z', 'undo'],
|
['z', 'undo'],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import type { FeedbackModeUI } from './types';
|
import type { FeedbackModeUI } from './types';
|
||||||
|
import { DiceIcon } from './icons';
|
||||||
|
|
||||||
function ThumbIcon({ size = 24, down = false }: { size?: number; down?: boolean }) {
|
function ThumbIcon({ size = 24, down = false }: { size?: number; down?: boolean }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -44,6 +45,10 @@ export interface VerdictClusterProps {
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onCommit: () => void;
|
onCommit: () => void;
|
||||||
onReroll: () => void;
|
onReroll: () => void;
|
||||||
|
/** Small bounded weight perturbation of the current net (left half of the pill). */
|
||||||
|
onNudge: () => void;
|
||||||
|
/** Full re-roll of the current net (right half of the pill). */
|
||||||
|
onRandomise: () => void;
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
ab: 'A' | 'B';
|
ab: 'A' | 'B';
|
||||||
onToggleAB: () => void;
|
onToggleAB: () => void;
|
||||||
|
|
@ -62,6 +67,8 @@ export function VerdictCluster({
|
||||||
onUndo,
|
onUndo,
|
||||||
onCommit,
|
onCommit,
|
||||||
onReroll,
|
onReroll,
|
||||||
|
onNudge,
|
||||||
|
onRandomise,
|
||||||
canUndo,
|
canUndo,
|
||||||
ab,
|
ab,
|
||||||
onToggleAB,
|
onToggleAB,
|
||||||
|
|
@ -116,6 +123,22 @@ export function VerdictCluster({
|
||||||
...extra,
|
...extra,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Two halves of the secondary pill (nudge | randomise).
|
||||||
|
const pillHalf = (extra: CSSProperties): CSSProperties => ({
|
||||||
|
flex: 1,
|
||||||
|
height: 30,
|
||||||
|
border: 0,
|
||||||
|
background: 'transparent',
|
||||||
|
color: 'var(--fg-mute)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 'var(--fs-xs)',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
transition: 'background var(--dur-fast), color var(--dur-fast)',
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onPointerEnter={() => setHover(true)}
|
onPointerEnter={() => setHover(true)}
|
||||||
|
|
@ -126,75 +149,138 @@ export function VerdictCluster({
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 'var(--sp-3)',
|
gap: 'var(--sp-2)',
|
||||||
padding: 'var(--sp-2) var(--sp-3)',
|
|
||||||
background: 'var(--glass)',
|
|
||||||
backdropFilter: 'blur(14px)',
|
|
||||||
WebkitBackdropFilter: 'blur(14px)',
|
|
||||||
border: '1px solid var(--glass-line)',
|
|
||||||
borderRadius: 'var(--r-pill)',
|
|
||||||
boxShadow: 'var(--shadow-2)',
|
|
||||||
opacity: hover || firstSession ? 1 : 0.55,
|
opacity: hover || firstSession ? 1 : 0.55,
|
||||||
transition: 'opacity var(--dur-med) var(--ease-console)',
|
transition: 'opacity var(--dur-med) var(--ease-console)',
|
||||||
zIndex: 40,
|
zIndex: 40,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
type="button"
|
style={{
|
||||||
title={downTitle}
|
display: 'flex',
|
||||||
onPointerDown={perturbDown}
|
alignItems: 'center',
|
||||||
onPointerUp={perturbUp}
|
gap: 'var(--sp-3)',
|
||||||
onPointerLeave={() => {
|
padding: 'var(--sp-2) var(--sp-3)',
|
||||||
if (lp.current) clearTimeout(lp.current);
|
background: 'var(--glass)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
WebkitBackdropFilter: 'blur(14px)',
|
||||||
|
border: '1px solid var(--glass-line)',
|
||||||
|
borderRadius: 'var(--r-pill)',
|
||||||
|
boxShadow: 'var(--shadow-2)',
|
||||||
}}
|
}}
|
||||||
style={big(
|
|
||||||
explore
|
|
||||||
? exploring
|
|
||||||
? { background: 'rgba(0,204,255,0.16)', color: 'var(--accent-2)' }
|
|
||||||
: { background: 'rgba(0,204,255,0.10)', color: 'var(--accent-2)' }
|
|
||||||
: { background: 'rgba(255,68,102,0.16)', color: 'var(--danger)' },
|
|
||||||
)}
|
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
|
||||||
>
|
>
|
||||||
{/* Explore-mode down is a re-roll/explore (↻), not a dislike thumb. */}
|
<button
|
||||||
{explore ? <span style={{ fontSize: 24, lineHeight: 1 }}>↻</span> : <ThumbIcon down />}
|
type="button"
|
||||||
</button>
|
title={downTitle}
|
||||||
|
onPointerDown={perturbDown}
|
||||||
|
onPointerUp={perturbUp}
|
||||||
|
onPointerLeave={() => {
|
||||||
|
if (lp.current) clearTimeout(lp.current);
|
||||||
|
}}
|
||||||
|
style={big({
|
||||||
|
background: explore && exploring ? 'rgba(255,106,0,0.22)' : 'rgba(255,106,0,0.16)',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
})}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||||
|
>
|
||||||
|
{/* Explore mode = dice (re-roll into a scratchpad); dislike mode = thumb-down. */}
|
||||||
|
{explore ? <DiceIcon size={24} /> : <ThumbIcon down />}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Undo (z)"
|
title="Undo (z)"
|
||||||
onClick={onUndo}
|
onClick={onUndo}
|
||||||
disabled={!canUndo}
|
disabled={!canUndo}
|
||||||
style={big({
|
style={big({
|
||||||
width: 48,
|
width: 36,
|
||||||
height: 48,
|
height: 36,
|
||||||
fontSize: 20,
|
fontSize: 16,
|
||||||
background: 'var(--bg-2)',
|
background: 'var(--bg-2)',
|
||||||
color: 'var(--fg-mute)',
|
color: 'var(--fg-mute)',
|
||||||
opacity: canUndo ? 1 : 0.4,
|
opacity: canUndo ? 1 : 0.4,
|
||||||
cursor: canUndo ? 'pointer' : 'not-allowed',
|
cursor: canUndo ? 'pointer' : 'not-allowed',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
↺
|
↺
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title={upTitle}
|
title={upTitle}
|
||||||
onClick={onCommit}
|
onClick={onCommit}
|
||||||
style={big({
|
style={big(
|
||||||
background: picking ? 'rgba(0,204,255,0.22)' : 'rgba(255,106,0,0.18)',
|
picking
|
||||||
color: picking ? 'var(--accent-2)' : 'var(--accent)',
|
? {
|
||||||
boxShadow: picking ? '0 0 16px var(--accent-2)' : '0 0 16px var(--glow-accent)',
|
background: 'rgba(0,204,255,0.22)',
|
||||||
})}
|
color: 'var(--accent-2)',
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
boxShadow: '0 0 16px var(--accent-2)',
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
}
|
||||||
|
: {
|
||||||
|
background: 'rgba(107,194,107,0.20)',
|
||||||
|
color: 'var(--good)',
|
||||||
|
boxShadow: '0 0 16px rgba(107,194,107,0.45)',
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.08)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||||
|
>
|
||||||
|
{/* Explore-mode up is "place" (a pin glyph) once exploring. */}
|
||||||
|
{explore && exploring ? <span style={{ fontSize: 22, lineHeight: 1 }}>⌖</span> : <ThumbIcon />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary pill — two halves: nudge (small perturbation) | randomise (full re-roll). */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
width: 220,
|
||||||
|
background: 'var(--glass)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
WebkitBackdropFilter: 'blur(14px)',
|
||||||
|
border: '1px solid var(--glass-line)',
|
||||||
|
borderRadius: 'var(--r-pill)',
|
||||||
|
boxShadow: 'var(--shadow-2)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Explore-mode up is "place" (a pin glyph) once exploring. */}
|
<button
|
||||||
{explore && exploring ? <span style={{ fontSize: 22, lineHeight: 1 }}>⌖</span> : <ThumbIcon />}
|
type="button"
|
||||||
</button>
|
title="Nudge — small bounded perturbation of the current net"
|
||||||
|
onClick={onNudge}
|
||||||
|
style={pillHalf({ borderRight: '1px solid var(--glass-line)' })}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255,106,0,0.12)';
|
||||||
|
e.currentTarget.style.color = 'var(--accent)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = 'var(--fg-mute)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
nudge
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Randomise — full re-roll of the current net"
|
||||||
|
onClick={onRandomise}
|
||||||
|
style={pillHalf({})}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255,106,0,0.12)';
|
||||||
|
e.currentTarget.style.color = 'var(--accent)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = 'var(--fg-mute)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
randomise
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,22 @@ export function HelpIcon({ size = 18, style }: IconProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dice — explore / re-roll into a scratchpad (a die showing five pips). */
|
||||||
|
export function DiceIcon({ size = 22, style }: IconProps) {
|
||||||
|
return svg(
|
||||||
|
size,
|
||||||
|
style,
|
||||||
|
<>
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="3" />
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.1" fill="currentColor" stroke="none" />
|
||||||
|
<circle cx="15.5" cy="8.5" r="1.1" fill="currentColor" stroke="none" />
|
||||||
|
<circle cx="12" cy="12" r="1.1" fill="currentColor" stroke="none" />
|
||||||
|
<circle cx="8.5" cy="15.5" r="1.1" fill="currentColor" stroke="none" />
|
||||||
|
<circle cx="15.5" cy="15.5" r="1.1" fill="currentColor" stroke="none" />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Close (✕). */
|
/** Close (✕). */
|
||||||
export function CloseIcon({ size = 14, style }: IconProps) {
|
export function CloseIcon({ size = 14, style }: IconProps) {
|
||||||
return svg(size, style, <path d="M6 6l12 12M18 6 6 18" />);
|
return svg(size, style, <path d="M6 6l12 12M18 6 6 18" />);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export interface Snapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
|
export type DrawerKey = 'learn' | 'inputs' | 'route' | 'settings' | 'help';
|
||||||
export type DrawerDepth = 'peek' | 'expand' | 'full';
|
export type DrawerDepth = 'condensed' | 'expanded';
|
||||||
export type Focus = 'in' | 'split' | 'out' | 'composite';
|
export type Focus = 'in' | 'split' | 'out' | 'composite';
|
||||||
|
|
||||||
export interface Axes {
|
export interface Axes {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ test('engine loads, spine propagates, console renders', async ({ page }) => {
|
||||||
// 4. The convertible Console rendered (assert on the dock drawer rail, which
|
// 4. The convertible Console rendered (assert on the dock drawer rail, which
|
||||||
// is always present — the old "MEMLNaut" wordmark only shows outside
|
// is always present — the old "MEMLNaut" wordmark only shows outside
|
||||||
// Particle mode now that the particle top bar is a heatmap strip).
|
// Particle mode now that the particle top bar is a heatmap strip).
|
||||||
await expect(page.getByText('Learning')).toBeVisible();
|
// Drawers are closed by default, so assert on the rail button's title
|
||||||
|
// rather than the (hidden) drawer-header text.
|
||||||
|
await expect(page.getByTitle('Learning')).toBeVisible();
|
||||||
|
|
||||||
// 5. No "C15" anywhere in the rendered UI.
|
// 5. No "C15" anywhere in the rendered UI.
|
||||||
const body = await page.evaluate(() => document.body.innerText);
|
const body = await page.evaluate(() => document.body.innerText);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue