Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0854c3b
[feat] controls ride above the bottom dock
AlexZ005 Aug 27, 2026
cb3c059
[feat] one decision tree for the panel buttons and their keys
AlexZ005 Aug 27, 2026
739f9df
[feat] the explorer is a bottom-dock tab
AlexZ005 Aug 27, 2026
ee786fc
[feat] play mode menu: right-click the play button for desktop/VR/AR
AlexZ005 Aug 27, 2026
55f8b5c
[feat] the controls pill is a customizable roster
AlexZ005 Aug 27, 2026
74398be
[feat] rebindable shortcuts and a play key
AlexZ005 Aug 28, 2026
eed6221
[fix] toolbar menus: sticky customize, one-slot moves, menu-only expand
AlexZ005 Aug 28, 2026
c5d23d8
[feat] floating toolbar is opt-in; the dock gets minimize and close-tab
AlexZ005 Aug 28, 2026
65212eb
[fix] instant play re-entry; a denied XR permission recovers
AlexZ005 Aug 28, 2026
b8e9a84
[feat] dock strip polish and the rebindable G grab key
AlexZ005 Aug 29, 2026
87ca571
[feat] mobile play mode: left half walks, right half looks, X and Bac…
AlexZ005 Aug 29, 2026
54a1e64
[fix] slim dock tabs and a working dock resize; explorer close, no do…
AlexZ005 Aug 29, 2026
f0514cd
[feat] the toolbar floats by default, stays on top, and moves
AlexZ005 Aug 29, 2026
253c497
[feat] the bottom dock resizes the viewport instead of covering it
AlexZ005 Aug 29, 2026
0cd8238
[fix] the customize list is the bar; every dock view can be a button
AlexZ005 Aug 29, 2026
18f188e
[feat] drag dock tabs to reorder, out to undock, windows in to dock
AlexZ005 Aug 29, 2026
44cece7
Merge feat/controls-toolbar-round2 into feat/controls-dock-rework
AlexZ005 Aug 29, 2026
642fd44
[fix] dock-tab-drag: park the dragged header clear of the floating to…
AlexZ005 Aug 29, 2026
9d116eb
[fix] the toolbar re-measures its track when the dock's inset changes
AlexZ005 Aug 29, 2026
a87529f
[fix] every floating window raises and hides; click brings one to front
AlexZ005 Aug 30, 2026
b6802eb
[feat] a shortcut for every panel; the shader editor undocks
AlexZ005 Aug 30, 2026
0a5563f
Merge feat/controls-round4: panel shortcuts and the shader editor's w…
AlexZ005 Aug 30, 2026
3ce32cf
[fix] the flowCode key mismatch, three rotted glyph selectors, stale …
AlexZ005 Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,15 @@ loadable play content. Everything a user does must be visible to connected peers
Multi-slot objects are REFUSED, the switchMaterialType precedent. UI:
`components/editors/ShaderEditor.svelte` (a FLOW_FAMILY dock tab, its own xyflow
instance so flowGraphs/nodesync stay byte-untouched; scope follows the SELECTION
like the node editor's flow graphs, so there is no scope control) +
like the node editor's flow graphs, so there is no scope control. It was the ONE
dock view with NO FLOATING MODE at all — no `docked` flag, no dragWindow, no window
chrome — and TWO other modules carried an exception for that fact (`panelToggles`'
`dockOnly` shape, and `dockMenu.dockTabItems` withholding "Undock" rather than
shipping a row that could only do nothing). It has UvEditor's docked/floating split
now — `shaderDocked` + `#shader-window` with dragWindow / a KEYED focusStack /
tabbable / bottomDockable / a corner grip, plus the `dockModeArm` consumer that is
what makes Undock and drag-a-tab-out reach it — so both exceptions are gone and the
seven dock views behave identically) +
`ShaderSidebar.svelte` + `nodes/ShaderNode.svelte` (ONE generic node for the whole
catalog) + `nodes/ShaderTexturePicker.svelte` (a file input that imports into the
Explorer, an Explorer drag-drop target, thumbnail, clear, and a "waiting for peer"
Expand Down
50 changes: 44 additions & 6 deletions src/App.svelte

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion src/components/ContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
import { autofocusOk, typeToFocus } from '$lib/inputDevice';

// Generic context menu. items: [{ label, action?, disabled?, tooltip?, danger?,
// icon?, hint?, checked?, children?: items[] } | { section } | { header }]
// icon?, hint?, checked?, keepOpen?, rowActions?, children?: items[] } |
// { section } | { header }]
//
// W1: `keepOpen: true` runs the action and LEAVES THE MENU UP — opt-in per item,
// so every other menu in the app closes on a click exactly as before. It exists for
// a menu that is a CHECKLIST rather than a command list (the toolbar's Customize
// pane): toggling one button's visibility used to dismiss the list you were working
// through. Outside click and Escape still close. The item array is a prop, so a
// consumer that wants the rows to show the state it just wrote must pass a REACTIVE
// array (Controls derives its Customize items from the layout record) — re-rendering
// in place keeps the menu's position and scroll, because the node is never replaced.
// Submenus (any depth) open on hover, marked with ▸. Flips up/left near screen edges.
//
// 15-Q: dense menus grew a TYPE-TO-FILTER row (flattened command-palette matches).
Expand Down Expand Up @@ -53,6 +63,7 @@
return;
}
item.action?.();
if (item.keepOpen) return;
dispatch('close');
}

Expand Down
93 changes: 92 additions & 1 deletion src/components/ContextMenuItems.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,36 @@
// 16-P3 adds `checked: true` — the ACTIVE choice of a group (bold + accent),
// which replaced the old '● ' label prefix.
//
// W1 adds two more:
// keepOpen: true the row's action runs and the menu STAYS UP (owned by
// ContextMenu's `run`, documented there)
// rowActions: [{ icon, label, tooltip?, disabled?, run }]
// small trailing controls INSIDE the row — the toolbar's
// Customize list needs a reorder pair beside each button, and
// a row that both toggles and reorders cannot say that with a
// label. They are inline CONTROLS, not menu commands: the
// click never reaches the row (stopPropagation) and never
// closes the menu, so `keepOpen` does not apply to them.
// `label` is both the tooltip and the accessible name.
//
// W8b adds one:
// key: 'move' a STABLE identity for a row in a list that REORDERS itself.
// The each block below is keyed on it, so when a `keepOpen`
// action rewrites the array into a new order svelte MOVES each
// row's DOM node instead of rewriting the labels in place. That
// is what makes a per-row reorder control usable: the button
// you just pressed travels with its row, stays focused, and a
// second press walks the same item another slot. Without it the
// node stays put, the row under your finger becomes a DIFFERENT
// item, and pressing again undoes what you just did.
// Optional and namespaced against the index fallback, so every
// menu that passes no key is keyed by position — byte-identical
// to the unkeyed block this replaces.
//
// 16-P1: which submenu is open (`openPath`) and where the keyboard cursor sits
// (`navPath` + `highlight`) are owned by ContextMenu — ONE truth shared by mouse
// and keyboard. Hover-intent lives here: 120ms to open, 150ms to close.
import { tick } from 'svelte';
import Icon from './ui/Icon.svelte';
export let items: any[] = [];
export let onrun: (item: any) => void;
Expand All @@ -37,6 +64,26 @@
let openTimer: any = null;
let closeTimer: any = null;

/** W8b: run a row's inline control and KEEP THE KEYBOARD ON IT.
*
* A reorder control rewrites the list it lives in, and the keyed each block then
* RELOCATES this row's node to its new slot — which Chromium treats as a blur, so
* the control you just pressed silently stops being focused and a repeat press has
* nowhere to go. The node survives (that is what the key buys); only the focus does
* not. Re-focusing it by its own accessible NAME is what makes "press again to move
* it again" true for the keyboard as well as the mouse — the label names the BUTTON,
* so it follows the item wherever the move put it, and a control that has become
* disabled at an end of the list is simply not re-focused. */
async function runRowAction(act: any, host: HTMLElement) {
act.run?.();
await tick();
const root = host.closest('[role="menu"]') ?? document;
const again = root.querySelector<HTMLButtonElement>(
`button.ctx-act[aria-label="${String(act.label).replace(/"/g, '\\"')}"]`
);
if (again && !again.disabled) again.focus({ preventScroll: true });
}

/** the child submenu open at THIS level (null = none) */
$: openChild = openPath.length > path.length ? openPath[path.length] : null;
/** is the keyboard cursor on this level? */
Expand Down Expand Up @@ -113,7 +160,7 @@
const disabledClass = 'cursor-default px-3 py-1.5 text-gray-400 dark:text-gray-500 whitespace-nowrap';
</script>

{#each items as item}
{#each items as item, rowAt (item?.key != null ? 'k:' + item.key : 'i:' + rowAt)}
{#if item.header}
<!-- 15-Q: target strip — WHAT this menu acts on (kills the counted-label
ambiguity); locked state lives here instead of scattered tooltips -->
Expand Down Expand Up @@ -187,6 +234,7 @@
data-ctx-active={atNav && indexOf.get(item) === highlight}
role="menuitem"
title={item.tooltip ?? ''}
aria-label={item.rowActions ? item.label : undefined}
on:mouseenter={() => hoverRow(item, indexOf.get(item) ?? -1)}
on:mouseleave={leaveRow}
on:click={() => onrun(item)}
Expand All @@ -199,6 +247,22 @@
{#if item.hint}
<span class="ctx-hint">{item.hint}</span>
{/if}
{#if item.rowActions}
<span class="ctx-actions">
{#each item.rowActions as act}
<button
type="button"
class="ctx-act"
title={act.tooltip ?? act.label}
aria-label={act.label}
disabled={act.disabled}
on:click|stopPropagation={(e) => runRowAction(act, e.currentTarget)}
>
<Icon name={act.icon} size={13} />
</button>
{/each}
</span>
{/if}
</span>
</div>
{/if}
Expand Down Expand Up @@ -269,6 +333,33 @@
display: inline-flex;
color: rgb(148 163 184);
}
/* W1: inline controls at the end of a row (the Customize list's reorder pair).
Muted until hovered so the row still reads as its label first. */
.ctx-actions {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 2px;
margin-left: 10px;
}
.ctx-act {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 4px;
color: rgb(148 163 184);
background: transparent;
}
.ctx-act:hover:not(:disabled) {
color: inherit;
background: rgb(148 163 184 / 0.25);
}
.ctx-act:disabled {
opacity: 0.35;
cursor: default;
}
.ctx-hint {
flex: 0 0 auto;
margin-left: 12px;
Expand Down
Loading