[feat] controls and bottom-dock rework: ride-above dock, unified toggles, explorer tab, play mode menu, roster pill, rebindable shortcuts - #183
Merged
Conversation
- the Controls pill + play FAB anchor on --bottom-inset (the visible docked panel's height, already JS-published by bottomDock) so they sit in the band just above an open Flow/Explorer dock instead of covering its last ~60px of thumbnails/palette on desktop; 200ms bottom transition matches the dock's own fly - the old --dock-inset model is deleted (declaration, the <=500px media rule, and its three consumers in Flow/Explorer/ShaderEditor) - it padded the dock's CONTENT and only on folded screens, leaving every wider screen with the pill permanently over the dock - Inspector/NotesDrawer bottom offset becomes bottom-inset + controls-inset (sum, not max: the pill no longer sits on the viewport bottom, so the taller-of-the-two no longer clears both; identical whenever either is 0) - cameraPip autoPosition gains bottomClearance and the PiP window passes $bottomInset, so a parked PiP is no longer covered by the dock; ColocationBadge rides the same var - the play FAB's transition moves inline (transform 100ms, bottom 200ms) - an inline transition shorthand replaces the class-set one, so extending the tailwind class would have silently killed the hover-scale - dock-inset suite: 5 new checks proving the ride-up (FAB/pill above the dock top while open, back at their 10px/16px offsets when closed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new $lib/panelToggles: togglePanel('objects'|'flow'|'explorer') is the ONE
tree behind the Controls toolbar buttons AND the O/N shortcuts, which used
to bypass the buttons with bare store flips - closed opens in the last
mode (dock tab or floating, revealed + raised), a buried floating window
is RAISED instead of closed (the object-list semantics, generalized), a
window in a tab group activates its tab, a docked panel covered by another
tab is brought back, and only a panel already on top hides
- the Node editor keeps its flowDockSnapshot behavior verbatim (hide/restore
exactly the flow-family tabs that were docked, incl. the load-bearing A4
hud line); a floating Explorer no longer repoints bottomDockActive at a
panel that is not in the dock
- windowFocus gains isTopVisibleWindow: closed windows stay MOUNTED (hidden
class / display:none) and the focus order only drops a node on destroy, so
a window closed while on top read as top forever and the plain isTopWindow
check misfired on the next press
- flow/explorer floating windows register focusStack KEYS at last, so
raiseWindow can address them; windowTabs exports groupOfKey
- flow-explorer-floating rewritten for raise-then-hide (10 checks); new
panel-toggle-keys suite (17 checks) pins keyboard parity; three guards
proven by breaking the code (always-close, isTopWindow swap, bare flips)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- one panel, seven tabs (VS Code bottom-panel model): DOCK_FAMILY = FLOW_FAMILY + explorer, a dockTabs derived the strip renders, and the exclusivity subscription DELETED - activating a flow tab used to force- CLOSE the explorer while the flow family merely hid, so switching tabs now changes only which panel renders and nothing ever closes anything (flowTabs stays exported and flow-family-only: the node-editor button owns that group and the flow-dock suites read it) - new $lib/dockMenu dockAddItems(): the dock's "+" menu in ONE place - DockTabs and the floating node editor's "+" kept separate copies of the same list (the documented drift), and the explorer is an entry now - the explorer's docked branch renders the DockTabs strip and adopts the SHARED dockHeight (one value for every tab; resize writes through the store; one-shot migration adopts a legacy explorerHeight then drops the key); floating explorerWinW/H untouched; bottomDock now imports zero app stores - suites: flow-explorer-dock + dock-float-exclusivity rewritten for coexistence, panel-toggle-keys updated (a covered explorer tab is a reachable state now), new dock-tabs-coexist (15 checks incl. the height migration); the counterfactual is proven - re-adding the exclusivity subscription turns 6 checks red across four suites Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new $lib/playMode owns the play state machine the FAB, its new menu and
the coming keyboard shortcut all share: the isSessionSupported probes,
willEnterXR/willEnterAR, requestPlay() and the 2s exit cooldown with the
21-F3 queued-press replay (an isLocked.subscribe that hops out through
setTimeout - never a store write inside its own subscriber)
- the decision no longer sniffs threlte's private button label: the probes
decide, the label survives only as a belt-and-braces guard against
clicking an in-session Exit button
- BOTH hidden XR buttons mount permanently (#vrButtonVr/#vrButtonAr,
data-aim echoes the preference): the old {#if $vrPassthrough} swap was a
reactive REMOUNT, fine while the preference only changed in Settings and
a race the moment a menu pick flips it and enters in the SAME gesture -
with both mounted the aim is a query and requestSession stays
user-gesture-synchronous
- right-click (a direct listener, not the deprecated on:contextmenu
directive) on #play-button opens the shared ContextMenu: Play (desktop) /
Enter VR / Enter AR passthrough as a checked radio group, unsupported
modes disabled with the reason, each pick writing the preference (store +
localStorage, the Settings shapes) and entering immediately; iOS Safari
fires no contextmenu on long-press, so Settings keeps the toggles
- the pre-existing isLocked Writable<null> TS error moved here with the
code it belongs to - baseline stays exactly 385/62, an in-file comment
stops anyone tidying it into a silent baseline shift
- new play-mode-menu suite (24 checks); vr-passthrough retargeted onto the
dual mount + data-aim; counterfactual proven (reverting to the {#if}
swap turns 6 checks red)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the pill is a plain nav (#controls-pill) rendering a persisted layout
{order, hidden, spacerIndex, collapsed} through ONE {#each} template -
flowbite's BottomNav could not take a dynamic inner grid (its classes
are JIT literals and restProps land on the outer div), so the nav bakes
in flowbite's own resolved classes, measured pixel-identical to HEAD
- the play FAB moves INSIDE the spacer well (top -5px against the 38px
row reproduces the old 4px/6px overhang), so it inherits the pill's
ride above the dock and tracks the well wherever customization puts it;
the right hover-paint half is addressed as nth-child(2) because the FAB
became the well's third child
- right-click any cell: Move left/right (a button swaps with its
neighbour, skipping the well; the FAB's own menu walks the well),
Hide button (never offered on the FAB - you cannot hide the way out),
Collapse toolbar (well + expand chevron only), Customize toolbar
(checked visibility rows + a danger Reset restoring the default and
clearing the pref); the node-editor cell also offers the shared dock
"+" list and the explorer cell Open-as-dock-tab / Open-as-floating
- explorer dock-mode rows go through a new explorerDockArm store seam
(the explorerSceneSaveArm shape): explorerDocked is read ONCE at mount
into component-local state, so a toolbar localStorage write is inert at
a live panel - the toolbar asks, setDocked (which owns the flag, the
render branch and the occupancy together) acts
- svelte-check 385 errors / 47 WARNINGS: the six hand-written cells each
carried the same three a11y/directive warnings and one template reports
them once - a legitimate ratchet from 62 (the release.yml gate must
move to 47 when this lands)
- new controls-roster suite (31 checks incl. reload persistence, proven
by neutering saveLayout); dock-inset's pill selector retargeted to
#controls-pill
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the shortcut registry gains stable ids and defaultKeys; user overrides
live in localStorage shortcutOverrides {id -> keys} and applyOverrides
rewrites each entry's keys in place, so the matcher, the Settings
renderer and editorNavigation's Shift probe never learn overrides exist;
the empty map stores nothing
- comboOf can express Alt now (canonical Ctrl+Alt+Shift+K; no shipped
default uses it, asserted in-suite, so every existing combo is
byte-identical); registerShortcut dedupes by id, not keys - keys-dedupe
breaks the moment an override frees a default combo
- rebindShortcut refuses rather than clobbers (fixed rows, bare
modifiers, the Escape deny-list, registry collisions returned as a
conflict the UI offers to Swap); per-row reset + Reset all; display-only
rows are fixed with the reason (movement keys, PTT hold, mesh-edit
session); conflictOf also warns when a combo sits on the MESH_EDIT_KEYS
stand-down list, which tests literal combos and so follows a rebind
- Settings > Shortcuts: click a key to rebind (window capture-phase
listener, Esc cancels, closing Settings tears it down), inline conflict
row with Swap, lock glyph + reason on fixed rows - the FILE IS LEGACY
MODE (a $state would flip it to runes and break the build), so the
redraw signal is a plain let + {#key}
- new scene.play entry: Ctrl+Enter -> requestPlay() (Ctrl+P is the
browser's print dialog and bare P is physics; Alt+P is now expressible
as a rebind); the modal exemption follows help.shortcuts by id instead
of its literal combo, so it survives a rebind
- shortcuts.js gains a capturing stand-down (setShortcutCapture) so the
registry never executes a combo the rebind UI is recording - proven by
neutering it and watching the suite go red
- new shortcut-rebind suite (43 checks incl. reload persistence, swap,
refusals, the standdown, and Ctrl+Enter entering play); 13 regression
suites green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ContextMenu items gain keepOpen (opt-in; every other menu byte- unchanged): the Customize list is a CHECKLIST, and toggling one button used to dismiss it - the rows are a $derived of controlsLayout now, so a keepOpen action re-renders them in place with position and scroll kept (the node is never replaced) - ContextMenuItems gains rowActions - small trailing controls inside a row (stopPropagation, never close): the Customize list reorders each button with an inline pair; a row carrying them takes an explicit aria-label or the buttons' names leak into the row's accessible name - Move left/right acts on the VISUAL row (buttons + the play well spliced in) and moves exactly ONE slot per press - swapping with the well crosses play and shifts the well by one, instead of the reported two-cell swap around the play button; order + spacerIndex are read back off the mutated sequence, one source of truth - collapsed toolbar renders the well alone: the expand chevron is gone, the FAB's right-click menu swaps Collapse for "Expand toolbar", and controlsLayout joined resetWindowLayout's wipe list + a live resetter (the app's existing put-my-chrome-back button is the iOS hatch, since Safari fires no contextmenu) - viewport right-click > Tools gains "Node editor" through the same togglePanel tree the N key and the toolbar button call - reset-windows suite FIXED, not skipped: it clicked the Scene accordion while #reset-windows lives under Interface, and flowbite renders no body for a collapsed item - the button was never in the DOM (its standing red had this one cause) - controls-roster grown 34 -> 60 checks; new viewport-tools-menu (11); keepOpen proven by removing the flag and watching the stays-open check go red Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new LOCAL pref floatingToolbar (Settings > Interface, default OFF after the on-device pass): ON = the pill + play FAB ride above the dock on --bottom-inset (the previous behavior); OFF = the pill is an ordinary member of the z-30 bottom-HUD tier, pinned 16px off the viewport floor, and an open dock covers it exactly like the chat/AI/sim buttons beside it - one derived class/style pair, no second markup branch (the FAB's inline z-hud cannot escape the pill's stacking context, asserted by elementFromPoint in dock-inset) - the tab strip carries the dock's own chrome now: "-" minimizes the whole dock (tabs stay open, nothing renders, bottomInset reads 0 - deliberately NOT persisted: a minimized dock leaves no trace on screen, so a reload brings it back rather than handing someone a lost panel) and "x" closes the ACTIVE tab (visibleDockKey, not bottomDockActive - the strip is drawn by whichever panel actually renders), letting the existing fallback promote the next tab - activateDock clears the minimize - every restore path (toolbar buttons, O/N keys, the "+" menu, a panel opening itself) already funnels through it; panelToggles' isVisibleInDock answers false while minimized, or step 4 would CLOSE the tab the user was asking to see - DOCK_CLOSERS/closeStoreFor live in dockMenu beside the add list (bottomDock keeps importing zero app stores) - dock-inset reworked: default asserts the pill stays put and is covered while the dock is open, then flips the real Settings toggle and asserts the ride-up; new dock-chrome suite (31 checks incl. the N-key restore, proven by removing the activateDock clear) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the flat 2s post-exit wall in playMode is GONE: the ~1s browser refusal after a user Esc belongs to the pointer lock, so PointerLockControls copes with it now - attempt immediately, retry on refusal (~275ms beat, 2.5s window, both failure signals: the modern promise rejection AND the legacy pointerlockerror event), stop the instant the lock lands or play is left; every non-Esc exit path stops paying 2s for a rule only Esc is subject to. lockRetryUntil is the ownership fence (the held-flag rule: only act on an error inside a window our own request opened); store reads go through get() so the isLocked effect cannot auto-subscribe to playPointerFree and fight the menu substate - 21-F3 survives as the sub-macrotask transient replay only (a press landing on isLocked===false is deferred to the settle, never eaten) - denying the VR/AR permission no longer wedges the app: isVRMode had ONE writer (the optimistic set before the click) and ONE reset (sessionend) - a rejected requestSession fires no session events and threlte's onerror prop was wired nowhere, so the flag stuck true and Scene.svelte's viewport-menu guard (the ONE writer of the viewport and object context menus) went silently dead along with click-select, the gizmo, WASD and focus. Both hidden buttons pass onerror now (xrSessionFailed, toast + reset), a 6s watchdog covers threlte's silent not-supported return, and Scene's sessionstart ASSERTS the flag (cancelling the watchdog), so a permission accepted late still comes up in VR - the optimistic set stays because Scene arms the VR configuration off it before the first XR frame - new play-reentry suite (32 checks; the timing check fires the press from the store subscription at the settle, because timing Playwright's .click() includes an actionability wait that let a 1500ms wall pass - the counterfactual re-added one and read 1342ms, red); game-presence section 2 rewritten to the user-facing property (leave mid-round, press play, back in the same round) since "inside the cooldown" is no longer a reachable premise; play-exits drops its 2200ms wait Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the dock's own chrome is a right-pinned cluster at the browser edge on the tab band (lucide Minus + X; the tab group gets right-24 + overflow-x-auto so a full strip scrolls under the cluster instead of running beneath it); the "+" becomes lucide Plus; icon-only buttons carry aria-labels beside their titles - the "+" menu lists only views NOT already docked (built per open off dockOccupants; all-docked degrades to one disabled row); the Node editor stays out of the base list on purpose - the same list is the floating Node editor's own header "+", where offering itself is nonsense - right-click (Android long-press) a TAB for Undock / Close on THAT tab, visible or not: the panels' docked flags are component-local, so the Explorer-only arm seam generalised into bottomDock's dockModeArm / armDockMode(key, docked) with a consume-effect in each of the six panels that own a flag (explorerDockArm deleted from appStore, its one consumer migrated); ShaderEditor genuinely has NO floating mode (no docked flag, no window, occupant report has no docked term) so its tab offers Close alone - it IS mounted while hidden, that was not the reason - G = grab, in three surfaces and rebindable: transform.grab (registry, Move mode, stands down during mesh sessions via the existing MESH_EDIT_KEYS list), uv.grab and animation.grab as a new EXTERNAL registry kind - listed and rebindable in Settings but owned by the editor's capture handler, which asks bindingOf(id) for the effective combo; the matcher skips external rows explicitly (proven by reordering the array, not by luck), and conflictOf collides only same-scope rows so three G defaults coexist while a real double-bind inside one scope is still caught - dock-chrome grown 30 -> 50 checks; new grab-key suite (36); three counterfactuals proven and restored byte-identical (the + filter, the external matcher guard, the conflict scope) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k exit - new TouchPlayControls overlay + touchControls leaf: a touch starting on the LEFT half of the canvas is a virtual move stick (64px radius, folded into the SAME moveX/moveY numbers the E5 gamepad mapping feeds, so walk mode, claims and the E3 menu substate gate it for free), the RIGHT half is look through the extracted applyLook (was three near-identical copies of the yaw/pitch clamp in PLC; touch is the fourth caller); gesture claim decided by TARGET (canvas only, pointerType touch only) so HUD buttons and hybrid mice are untouched - tap-to-interact survives: a still tap on the look half falls through to playInteract (travel alone discriminates - a redundant 250ms threshold measured 258.7ms on a real two-event tap and silently ate the interaction); touch presses never beginGrab (a swipe near a crate used to fling it); playInteract honors defaultPrevented from a claimed drag - REAL BUG: play mode left the canvas scrollable - OrbitControls is what sets touch-action none and it stands down in play, so Chromium pointercancel'd every drag after one frame (measured: a 110px look drag a third short, a stick applied 0.047 units once) - a touch-play-on root class pins the canvas while the overlay is up - exitPlay() in playMode is the canonical programmatic exit (release the lock when one is held, else the store write - the Esc pair); the X overlay (coarse pointers, isLocked, never during XR) and the Android Back button both call it - entering play pushes ONE history marker, popstate spends it on an exit instead of leaving the app, and every other way out consumes it so the next real Back is the user's (universal: desktop Back/back-swipe had the same surprise) - the W3 lock retry stands down on coarse pointers (no pointer to lock; it asked 9 times per entry); the gamepad enabled switch moved onto the pad snapshot so disabling the gamepad no longer disables the only control a phone has - svelte-check 384/47: the applyLook fold removed two baseline errors, exitPlay's isLocked.set(false) adds one of the documented writable(null) family - net minus one, the branch baseline restates to 384/47 - new touch-play suite (39 checks, REAL CDP touch - synthetic PointerEvents carry ids no browser issued and OrbitControls throws on setPointerCapture; explicit 390x844 viewport pins CDP input space to clientX); marker consumption proven by skipping history.back and watching the stale-marker checks go red Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ck X ONE root cause behind two reports: W5's lucide icon buttons dropped text-xs and gained pt-1.5 pb-1, so their 24px line-height made them 34px - and the strip is a flex row, so every text tab inherited it (measured 22 -> 34). That both fattened the tabs and, at -top-6, pushed the band's bottom edge 10px INSIDE the panel, burying the top-edge resize cue. - every strip button carries an explicit h-5.5 (22px) so one member can never set the band's height again; a Node editor tab measures 97.89 x 22 again, the pre-branch metric exactly (pinned in the suite) - the resize cue is raised to z-30, above the strip, with a hover tint beside its existing cursor/title affordance - a real-mouse drag at the middle of the top edge moved dockHeight 320 -> 320 before and 320 -> 380 now; dock-inset's own "inset tracks the resize" check had been failing 3/3 at HEAD and passes again - ShaderEditor gains the top-edge resize it NEVER had, so the shared dock height stopped being frozen whenever it was the visible tab; all seven panels behave identically now - the strip's X is gone (user testing: it read as "close the dock"); a view is closed by its own header X when floating or the tab's right-click menu when docked, and visibleDockKey's fallback still promotes the next tab - the Explorer's close button was never missing, it was UNREACHABLE: every header item is shrink-0, so the row's ~730px minimum overflowed the window's own 420px minimum and the trailing buttons sat outside an overflow-hidden window (measured x=743 against a right edge of 580) - the search+chips now live in a clipping min-w-0 flex-1 middle, and the button gained #explorer-close + an aria-label - minimize glyph -> lucide PanelBottom, picked by RENDERING ten candidates at 14px in the dark theme: it is the square-with-a-dash-at- the-bottom shape, while PanelBottomClose/Dashed smudge at that size, PanelBottomOpen reads as the opposite action, and SquareMinus's centred dash reads as a generic tree-collapse - dock-chrome 50 -> 64 checks; two counterfactuals prove the halves are independently load-bearing (the band height is the visual fix, z-30 is real defence in depth) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- floatingToolbar flips back to default ON and the bottom transition is DELETED from both modes (the FAB keeps transform 100ms for its hover scale). The default flip is a change to the STORED SHAPE, not just the seed: the reader becomes !== 'false', because every existing user has a literal 'false' on disk from the moment the pref shipped default-off (the subscriber writes on the first flush) and would otherwise be pinned off with no way to tell that from never having chosen - new toolbarAlwaysOnTop (default ON) splits Z-ORDER from GEOMETRY: floatingToolbar answers where the bar SITS (does it lift onto --bottom-inset), this answers who wins the pixel (z-45 over the dock and floating windows, or z-30 under them). They compose - the combination worth naming is floating OFF + on-top ON, where the bar stays on the viewport floor and an opening dock passes BEHIND it; only with both off does the dock cover it. The Settings copy no longer promises coverage the floating row no longer controls - the bar MOVES along the bottom: a pointerdown becomes a drag once it travels 6px (movement, never a timer - a finger held still has to stay the browser's long press, which is what raises the contextmenu every toolbar menu lives on), the play FAB is excluded from starting one, and "Move toolbar" / "Reset toolbar position" ride the shared tail. modalGrab owns the armed move (origin, snapshot, listeners, commit-or-revert) with arrows nudging through refresh() - the clamp is MEASURED, never coded: only a neighbour whose vertical band overlaps the bar's can be in the way, which is also why the <=600px HUD lift needs no breakpoint here (at that width chat/AI/mic move to a higher row and stop clamping). Recomputed on mount, resize, a ResizeObserver over the bar and each neighbour, and when the visible set changes; the position persists as a FRACTION of the track on controlsLayout, so it already rides resetWindowLayout - FOUND AND FIXED: modalGrab's capture-phase pointerdown stops a fresh gesture but not the browser's following CLICK, so committing an armed move over the Node editor cell opened the dock, which lifted the bar and widened the track - the position committed 21px from where it was placed. One window-level swallowNextClick replaces the per-handler flag guards - new toolbar-position suite (57 checks incl. the too-narrow fallback and drag-vs-click on the same cell both orders); dock-inset reworked for the new default and asserts "no animation" by DEMONSTRATION (a computed transition-property of `all` with 0s duration reads like everything animates and means the opposite) - side-edge (vertical) docking deliberately deferred, not half-landed: the well's nth-child hover halves, the rounded ends, the FAB's tuned overhang, a vertical track measured against different chrome, and --controls-inset's bottom-band contract are a phase of their own Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DCC behaviour: the canvas ends where the panel begins, rather than a full-window canvas with a panel drawn on top of it. - App.svelte wraps the Canvas in a .viewport region whose bottom is the open dock's height. That is the WHOLE rendering mechanism: threlte sizes the renderer from a ResizeObserver on the Canvas's PARENT, so the drawing buffer, camera aspect, composer and N8AO all follow from its own resize task, once per frame. No z-index (a stacking context would trap the PiP frame and the framing guide) and no transition (each step reallocates the composer's targets, so animating the inset turns one realloc into one per frame - which is also what the user asked for) - new dockPushesViewport pref in viewPrefs (default ON, Settings > Interface > Viewport); OFF restores the overlay behaviour exactly - the eight sites that mapped client coords to NDC through window.innerWidth/Height are fixed UNCONDITIONALLY through one canvasRect.js leaf (an un-inset canvas measures the window, so there is no mode to branch on): explorer drop, the module SDK's pointerRay, the mesh knife, adaptive vertex-handle sizing, pong, the framing guide, the camera PiP and the mobile add button. Two follow-ons: moduleSDK stores CLIENT coords and converts at ray time (the viewport can change while the pointer sits still), and cameraPip.autoPosition loses the bottomClearance argument added earlier this branch - once the caller passes the canvas, a second way to say one thing is a second way to get it wrong - bottomDock gains a viewportInset derived: the store twin of the CSS rule, so the canvas and the chrome measured against it cannot disagree - helpers.cjs projectPoint (SHARED by ~15 suites) had the same latent bug in the harness - it aimed clicks with window.innerHeight, so any suite opening the dock would have mis-aimed by its height. Now canvas-relative, arithmetically identical whenever the canvas fills the window - MEASURED, not assumed: a hand-paced top-edge drag with AO on at dpr 2 produced 61 distinct buffer sizes and ZERO frames over 20ms, matching the no-realloc control - so the drag stays live and unthrottled - new viewport-scale suite (34 checks); two counterfactuals, the second of which earned its keep: a VERTICAL knife cut passed with the fix reverted, because canvas and window share a width and the whole error lives in y - rewritten horizontally it goes decisively red (the same world point projects 174.9px apart), and the drop counterfactual misses by 100px at the tested pixel - the HUD frame deliberately stays window-relative: element offsets are PERSISTED against it, so moving it is a data migration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE ARROW "MESS" WAS THREE DEFECTS, reproduced before anything changed: - the list was ordered by the STORED order with hidden entries interleaved, while a move only ever swapped SHOWN buttons - so one press jumped the row over a hidden one, the moved row travelled TWO rows and the ticks leapfrogged the unticked one. The visibility bits were in fact preserved; it just read exactly like they were not - the rows re-sorted under a stationary pointer, so a second press grabbed whichever button had taken the slot and undid the first - the two ping-ponged and nothing could be walked anywhere - chevron-left/chevron-right were in NO icon map, and Icon.svelte ends `MAP[name] ?? Box`, so BOTH reorder controls had been drawing the same square since they shipped. There were never any arrows on screen - (a fourth, found by reading: an arrow whose neighbour was the play well rewrote only spacerIndex, so the press looked dead) FIX: the Customize list IS the bar, top-to-bottom = left-to-right, with the play well as its own row - one press is always one row, in the direction pressed, and it cannot touch a visibility bit. Rows carry a key so svelte RELOCATES the row's node instead of relabelling in place, and the control re-focuses by its accessible name afterwards (a relocation reads as a blur in Chromium). Arrows are chevron-up/down now, greyed with a reason on hidden rows; the horizontal bar's own cell menu keeps Move left/right. - the five remaining dock views (Flow Code, Animation, UV, Shader, HUD) are OPTIONAL roster buttons: absent from the bar until enabled in Customize, and the DEFAULT layout is byte-identical (an optional button is simply one in neither order nor hidden; loadLayout now filters against BUTTONS, where the old filter silently dropped every optional view on the next reload) - "Swap with" replaces a cell in place from the same DOCK_VIEWS data the "+" list is built from, so a button and its tab can never drift - togglePanel generalized from 3 entries to 8: one tree for every panel, so an Animation button behaves exactly as the Node editor button does, raise-a-buried-window and mode memory included. Shader is dockOnly - it has no docked flag, no dragWindow and no chrome, so without the flag the tree reads it as floating-only and raises a window that does not exist - icons picked from a RENDERED 18px sheet: Code / Clapperboard / Grid2x2 / Palette / Monitor. The sheet earned its keep - the obvious HUD glyph (layout-dashboard) and the obvious UV one (grid-2x2) are both a quartered square and indistinguishable at 18px; reading names would never have caught it - controls-roster 60 -> 102 checks; two counterfactuals (drop the row keying, restore the old row source) both proven and reverted byte-identical Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tab ORDER is user data now: a persisted dockTabOrder that dockTabs sorts by, with resolveOrder splicing an unknown key in beside its DOCK_FAMILY siblings (a view added in a later release appears where it belongs instead of vanishing or jumping to the front); reordering re-fills the PRESENT slots, so a closed tab never migrates - a tab drag promotes on 6px of TRAVEL, never a timer - a still press is what Android turns into contextmenu, so a timer would race the long-press menu; below the threshold it stays a plain click that activates the tab. Reorder geometry is frozen at gesture start so the live preview cannot move the boundaries deciding the next reorder - released outside the strip inflated by 44px (docking.js's own reach - the band is 22px tall, so a bare outside-the-rect test would undock on a wobble) the view UNDOCKS through the armDockMode seam - dragging a floating dock-family window into the bottom band docks it, in a sibling module rather than docking.js: use:dockable is wired on only three windows, two of which are dock tabs, so a bottom edge there would miss Flow Code / Animation / UV / HUD entirely, and wiring dockable onto them would hand them left/right edge docking nobody asked for - and the two docks are different models (an edge dock re-poses the node, the bottom dock asks the panel to re-render) - PRECEDENCE is explicit and asserted: header-merge (the smallest, most deliberate target) > bottom band > left/right edges - the tab menu gains Move left/right at the top, disabled at the ends - the drag is the fast way, these are the discoverable one and the only way to reorder without a pointer (touch stands down for both drags, as docking.js already does) - TWO REAL BUGS the tests caught: tabbable registers its pointerup first, merges, and hides the target, so re-asking headerTargetAt at the drop answered "no header" and docked ON TOP of a merge, dissolving it (the verdict is snapshotted on the last move now); and the band stole docking.js's bottom 44px for EVERY window including the object list, which can never be a dock tab, so its low edge-drop did nothing - new dock-tab-drag suite (47 checks). DRAG_PX=0 initially passed everything because mouse.click() emits no pointermove - a 3px-wobble check is what makes that counterfactual bite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane B (toolbar round 2) into Lane A (dock round 2): the toolbar floats by default and moves, the customize list is the bar and every dock view can be a button - joining the dock's slim tabs, working resize, viewport scaling and tab drag/dock/undock.
…olbar
The merge composed two correct behaviours into one collision: lane A's
section 8 parks the floating Node editor's header at the screen centre
under a 560px dock, and lane B's default-ON floating toolbar lifts the
pill onto --bottom-inset, which at that height puts it at y=104..144,
x=499..781 - exactly on that header, at z45 over the window's 43. The
grab hit a lucide path instead of the move handle. Neither lane alone
produces it (measured: floatingToolbar OFF puts the pill back at y=664
and the grab lands).
The source is right, so the fix is the suite's stale assumption: park at
x=400, which the 282px centred pill clears at every dock height, and
read the blocker with getAttribute('class') - .className on an SVG is an
SVGAnimatedString and printed [object SVGAnimatedString], naming nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reproduced at 1280x720: park the bar hard right with a 320px dock open, minimize the dock. --bottom-inset drops to 0, the bar falls from the band above the dock onto the chat/AI row - a row with different neighbours and a narrower track - with no re-measure, so it held right=1272 while #chat-button starts at 1220. It sat 52px over the button until an unrelated store change forced a measure and it snapped back to 1212. The measuring effect now voids bottomInset instead of visibleDockKey. That is the honest dependency: the pill's bottom resolves from --bottom-inset, which is what bottomInset publishes, so one line closes the whole class - minimize, restore, closing the last tab, undocking it, and switching to a tab of a different height. dockMinimized would have closed only the reported path, and visibleDockKey is subsumed (bottomInset derives from it plus dockMinimized plus the occupant heights); its icon-tint uses in the file are untouched. No re-clamp of the stored posX, deliberately: it is a FRACTION of the current track, so it is legal on any track and re-maps on its own (measured: stayed 1.000 across the row change and the bar re-centred itself). Rewriting it would forget the bar's place at the wide track, so restoring the dock would not put it back. 9 new checks in toolbar-position, proven red without the fix with the premises still passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TWO ROOT CAUSES behind "show/hide only works for the Explorer and the node editor", both measured before anything changed. - Of eleven use:focusStack sites only Flow, Explorer, the object list and two others pass a KEY. Flow Code, Animation, UV and HUD - exactly the four that gained roster buttons last round - registered with NO key, so windowFocus's byKey never held them and raiseWindow / isTopVisibleWindow could not address them: togglePanel's open-and- raise and hide-when-top steps silently no-opped for all four. Each now registers its PANELS key. - Click-to-front was never a missing listener (focusStack has had a capture-phase pointerdown raise since phase 82) - it was arithmetic. apply() ranked from the BOTTOM as `40 + min(index, 4)`, which clamps every window from the fifth onwards onto 44. Measured with seven panels open: flowcode/animation/uv/hud/explorer ALL read z=44, so raising one moved it in `order`, changed no number, and DOM order decided who painted. Ranking from the TOP makes the raised window the only 44 (measured 40/41/42/43/44 after), which is exactly the guarantee click-to-front needs; the band stays under --z-hud so the tier contract is untouched, and the collapse moves to the back of the stack where nobody is looking - the "Add a view" section leaves the Controls cell menu (the strip's + and the floating node editor's + keep dockAddItems, both verified still wired); Swap with and the toolbar tail are untouched - new floating-window-focus suite (37 checks). Its counterfactual is instructive: with the old clamp restored, the elementFromPoint check still PASSED because DOM order won the tie - the z assertions are what carry the weight Reported, not fixed: FlowCode's tabbable key is 'flowCode' while togglePanel looks for 'flowcode', so the tab-group branch cannot find it. Those keys are persisted in windowTabGroups, so renaming is a migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- seven new Panels rows, all rebindable like any other: bare T for the dock (show/hide, with an empty dock opening the last-active view - a restore to an empty dock is indistinguishable from the key doing nothing) and Alt+initial for the six views E/F/A/U/S/H. Bindings name the TOOL, so Alt+A opens Animation wherever it sits or even when it is not on the bar. conflictOf reported null for all seven before they were added, and asserts it again in-suite - the Alt prefix is load-bearing for THREE of the six, not all six: only E, F and S are bare mesh-session keys (measured - my first draft claimed all of them and the suite proved it false). All six take Alt anyway, because a scheme whose exceptions you must memorise is worse than one that is always true; proven by A/B on a live mesh session where bare S is swallowed and Alt+S opens - Alt+E/Alt+F are Chrome menu accelerators on Windows: measured that the keydown arrives and handleKeydown's preventDefault covers it (the bare Alt keydown stays unprevented, which is correct - Firefox only activates on an Alt keyup with no intervening key) - toggleDock asks through armDockMode rather than writing the panel's localStorage flag, which is inert at a live panel - so T genuinely docks a view whose remembered mode is floating - THE SHADER EDITOR HAS A FLOATING WINDOW at last, mirrored from UvEditor: docked flag, setDocked, the coarse-pointer guard, the dockModeArm consumer, #shader-window with dragWindow / focusStack KEYED 'shader' / tabbable / bottomDockable / corner grip, body and chrome as shared snippets so the two headers cannot drift. Both exceptions coded around its absence are gone: dockOnly in panelToggles (with its typedef field and two dead branches) and the guard withholding Undock in dockMenu; CLAUDE.md records that they were removed. W7's drag-out and drag-in work over DOCK_FAMILY, verified - new panel-shortcuts (117) and shader-window (40) suites; two suites RECORDED facts this batch changes and were flipped in-commit rather than worked around (shortcut-rebind's "no shipped default uses Alt", dock-chrome's "shader offers no Undock") - a third guard earned its keep unplanned: a premise check caught querySelector returning the HIDDEN Flow panel's copy of the strip (0x0 at the origin), so a synthesized drag was landing on the canvas and reading as a dead feature Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…indow Lane B (T + Alt+initial shortcuts for every panel, the shader editor's floating mode and the removal of both exceptions coded around its absence) into lane A (the focus-key and z-order fixes for floating windows).
…checks Merge verification of 0a5563f: no behavioural composition defect - the two lanes touch disjoint source files and all four risky interactions hold (Alt+S drives the shader window's full raise/hide cycle, T leaves floating z byte-identical across a minimize round trip, no orphan dockOnly reference survives, and the top window is uniquely 44 with eight panels open). What it found instead: - THE SHADER-EDITOR RED IS OURS, not pre-existing: origin/release/next renders the strip's + as text and passes with the same selector; the commits that made it a lucide icon are not ancestors of that branch. The earlier A/B compared against this branch's own HEAD, which already contained the change. Three suites were rotted the same way, not one (shader-editor, hud-editor, uv-editor); all select #dock-add-view now, and the tree was swept for the other old glyphs (-, x) with no further hits. shader-editor 0 -> 66 - THE flowCode/flowcode MISMATCH WAS WORSE THAN REPORTED. Three callers address windowTabs by the DOCK key, so the mismatch disabled the tab-group branch of togglePanel, the drags-as-one guard, AND headerTargetAt's self-exclusion - and that last one is user-facing: during a header drag the pointer is always on FlowCode's own header, so the drop test was false on every move and FLOW CODE COULD NEVER BE DRAGGED INTO THE DOCK. bottomDockDrop had documented the divergence as a quirk; it records it as the defect now. The tabbable key is renamed with a KEY_ALIASES migration where windowTabGroups is restored (members and active), while dragWindow deliberately KEEPS flowCode - it names a persisted rect and renaming it would strand saved positions - lane A's z assertion did not generalise: "every z distinct" held only because that suite opened five windows, which is the whole 40..44 band - a sixth correctly meets the documented 40 floor. Replaced with the guarantee the design actually makes (the top is uniquely 44, the stack stays under --z-hud), plus pixel ownership - coverage gap closed: lane A's focus suite predates lane B's shader window and was blind to it (37 -> 52); controls-roster still asserted the dock-only design lane B deleted, staying green only because the shader defaults to docked - uv-editor had a check asserting the opposite of this branch's own covered behaviour (the Explorer became a dock TAB and activateDock stopped closing anything), and its + check was VACUOUS - it searched every button and matched the strip's own tab, so it read green with the click doing nothing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- toolbarAlwaysOnTop defaults OFF: floating windows (z 40-44) and the
dock (35) paint over the Controls bar (30) unless the user opts in.
With floatingToolbar still default ON the bar rides above the dock's
band, so the two rarely overlap at all - the flip mostly decides that
a window dragged over the bar covers it, the bar getting out of the
way rather than fighting
- THE STORED KEY IS RENAMED (toolbarOnTop, was toolbarAlwaysOnTop): the
old subscriber wrote a literal 'true' onto every machine that ran the
branch on its first flush - a value no one chose - and a naive reader
flip would pin those machines ON indistinguishably from a choice. The
pref never shipped in a tagged release, so there is nothing real to
migrate; a suite check writes the OLD key and proves it is ignored
- "Always on top" joins the toolbar's own right-click tail as a checked
row (same store as the Settings row, so the two cannot disagree),
beside the position controls
- release.yml's baseline gate moves to 384/47 with the ratchet history
recorded (warnings 62 -> 47 from the roster {#each} consolidation,
errors -1 net from the applyLook fold)
- dock-inset reworked for the new default (the bar owns its pixel by
GEOMETRY on the lower tier; the covered mode needs only floating OFF
now; the floor + on-top combination asserted explicitly);
controls-roster drives the new menu row for the flip and closes the
floating Explorer its earlier section left open - at the new default
that window genuinely covers the play FAB, exactly as a real pointer
would find it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Controls / bottom-dock rework: five design phases, then two rounds of fixes from the user's on-device passes. Designed against DCC standards (VS Code's bottom panel, Unity's Shortcut Manager, Blender/Unity's editor-area layout, the mobile-FPS touch convention).
Round 1 — the rework
0854c3bControls above the dock — pill + play FAB anchor on--bottom-inset; the<=500px--dock-insetpadding hack deleted.cb3c059one decision tree for the panel buttons —$lib/panelTogglesshared by the buttons AND the O/N keys (open in last mode → raise a buried window → hide only when on top);windowFocus.isTopVisibleWindowfixes the stale-top-of-stack misfire.739f9dfthe Explorer is a dock tab —DOCK_FAMILY/dockTabs, the force-close exclusivity deleted, one shared dock height, one shared "+" builder.ee786fcplay mode menu — right-click the play button for Play / VR / AR, pick-and-enter in one gesture;$lib/playModeowns the probes andrequestPlay(); both hidden XR buttons mount permanently (kills the remount race and the threlte label sniff).55f8b5cthe pill is a persisted, customizable roster.74398berebindable shortcuts — stable ids + overrides, Alt-awarecomboOf, click-to-rebind with conflict Swap,Ctrl+Enter= play.Round 2 — first on-device pass
eed6221ContextMenukeepOpen+rowActions(the Customize checklist survives clicks); Move left/right acts on the VISUAL row, one slot per press; collapsed = the well alone with Expand in the menu; viewport Tools ▸ Node editor. Also root-caused the standingreset-windowsred (the suite clicked the wrong accordion).c5d23d8floating toolbar made opt-in; dock minimize + close-tab chrome.65212ebinstant play re-entry (the flat 2s wall replaced by a pointer-lock retry where the refusal actually lives) + XR-permission-deny recovery (isVRModehad one writer and no failure reset; the wedge was Scene's viewport-menu guard, the ONE writer of both context menus).b8e9a84right-pinned dock chrome, filtered "+" menu, per-tab Undock/Close via a generalized arm seam, and G = grab in viewport / UV / timeline (a newexternalregistry kind: listed and rebindable in Settings, executed by the owning editor).87ca571mobile play mode — left half walks, right half looks, a still tap interacts, X overlay and Android Back both exit. Found two real bugs: play mode left the canvas scrollable (Chromiumpointercanceled every drag after one frame) and a redundant 250ms tap threshold ate tap-to-interact.Round 3 — second on-device pass (this round)
54a1e64ONE root cause behind two reports: the round-2 lucide icon buttons droppedtext-xs, so their 24px line-height flex-stretched the tab band 22px → 34px — which both fattened the tabs and pushed the band's bottom edge 10px inside the panel, burying the resize cue. Tabs restored to 97.89 x 22px exactly; the cue raised above the strip in all seven panels, including ShaderEditor which never had one (so the shared dock height was frozen whenever it was showing). Dock X removed; the Explorer's close button turned out to exist but sit outside its own window at narrow widths; minimize glyph →PanelBottom, picked by rendering ten candidates at 14px.253c497the dock resizes the viewport instead of covering it (default on, revertible in Settings ▸ Interface ▸ Viewport). Rendering was free — threlte sizes from a ResizeObserver on the canvas's parent — so the work was the eight client→NDC sites that readwindow.innerHeightand were right only by accident (Explorer drop,api.pointerRay, the mesh knife, vertex-handle sizing, pong, the framing guide, the PiP, mobile add), all now unconditional through onecanvasRect.jsleaf. The same latent bug was in the test harness. Measured: a hand-paced resize drag with AO at dpr 2 = 61 buffer reallocations, zero frames over 20ms, so the drag stays live.f0514cdthe toolbar floats by default with the slide removed; always-on-top split from the floating geometry (the combination worth knowing: floating off + on-top on = the bar stays on the floor and the dock passes behind it); the bar drags horizontally with a clamp measured against whichever HUD buttons share its row. Found a realmodalGrabbug: its capture guard blocks a fresh gesture but not the browser's following click, so committing a move over a cell opened the dock and shifted the placement 21px.0cd8238the Customize "mess" was three defects, including thatchevron-left/chevron-rightwere in no icon map —Icon.sveltefalls back toMAP[name] ?? Box, so both reorder controls had been drawing the same square since they shipped. The list is now the bar itself (top-to-bottom = left-to-right, the play well as its own row); the five other dock views are optional roster buttons (defaults byte-identical); "Swap with" shares one data source with the "+" list;togglePanelwent 3 panels → 8.18f188edock tabs reorder by drag or menu with a persisted order, drag out to undock, drag a floating window in to dock; precedence header-merge > bottom band > left/right edges. Two bugs caught by its own tests: a header-merge was being docked on top of and dissolved, and the new band stole the object list's edge drop.44cece7/642fd44the two lanes merged and verified as a composition, not just as a text merge — one real defect found (the default-on floating toolbar covering the header a dock-drag test parked at screen centre under a 560px dock, measured at z45 over 43) and fixed in that suite, since both behaviours are correct.Gates
{#each}consolidation; errors −1 net from theapplyLookfold plusexitPlay). release.yml's hardcoded gate must move to 384/47 with this branch.npm run buildgreen at every commit and on the merge.Owed on-device (headless cannot judge)
🤖 Generated with Claude Code
Round 4 — third on-device pass
a87529ffloating show/hide fixed for every panel, and click-to-front fixed app-wide. Two root causes: four windows (Flow Code, Animation, UV, HUD) registereduse:focusStackwith no key, soraiseWindow/isTopVisibleWindowcould not address them and their roster buttons silently no-opped; andwindowFocus.apply()ranked the z band from the bottom (40 + min(index,4)), tying every window past the fifth at 44 — measured five windows at z=44 at once, so a click changed no number and DOM order decided who painted. Ranking from the top makes the raised window uniquely 44. "Add a view" also left the Controls cell menu (the strip's + keeps it).0a5563fa shortcut for every toolbar tool, and the Shader editor's floating window. BareT= dock show/hide;Alt+E/F/A/U/S/H= Explorer / Flow Code / Animation / UV / Shader / HUD;O/Nunchanged; every binding names the TOOL, not a toolbar slot, and all stay rebindable.conflictOfreported clear for all seven before they were added. Only E, F and S are bare mesh-session keys, but all six takeAlt+anyway — a scheme with memorised exceptions is worse than one that is always true (proven by A/B on a live mesh session: bareSis swallowed,Alt+Sopens). The Shader editor gained#shader-windowmirrored from the UV editor, and both exceptions coded around its absence are gone (dockOnlyin the toggle tree, the guard withholding "Undock" from its tab menu).3ce32cfmerge verification. No behavioural composition defect, but three things worth naming. Theshader-editorsuite's red was ours, not pre-existing — an earlier A/B compared against this branch's own HEAD, which already contained the change;origin/release/nextpasses with the same selector. Three suites were rotted by the +-to-lucide change, not one. And theflowCode/flowcodekey mismatch was worse than reported: three callers addresswindowTabsby the dock key, so besides the tab-group branch it brokeheaderTargetAt's self-exclusion — during a header drag the pointer is always on FlowCode's own header, so Flow Code could never be dragged into the dock at all. Renamed with awindowTabGroupsmigration (dragWindowdeliberately keeps its key — it names a persisted rect). Also corrected a z assertion that only held for five windows, a coverage gap blind to the eighth window, and auv-editorcheck that was vacuous (it matched the strip's own tab, so it read green with the click doing nothing).New suites this round:
floating-window-focus(52),panel-shortcuts(126),shader-window(40). Gates still 384 errors / 47 warnings, build green.