Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0383d9
[feat] 41-modals G3b: one z-index scale as tokens; modals cover the l…
AlexZ005 Oct 10, 2026
71efb07
[feat] 41-modals G16: blocking and destructive questions are kit moda…
AlexZ005 Oct 11, 2026
f0bab7d
[feat] 41-modals G19: settings rows reflow by their own width (contai…
AlexZ005 Oct 11, 2026
916a7a8
[feat] 41-modals G21: the Connect pill is a status dot, reconnecting …
AlexZ005 Oct 11, 2026
2de0191
[docs] 41-modals: CLAUDE.md z-index gotcha points at the one scale
AlexZ005 Oct 11, 2026
d009970
[test] 41-modals: fbInput pins the docked panel root by --z-dock (ren…
AlexZ005 Oct 11, 2026
5a57a08
[test] 41-modals: settings-narrow takes THEME= for the light-theme sc…
AlexZ005 Oct 11, 2026
9ef6314
[fix] 41-modals G21: the connecting dot is amber in light too (--stat…
AlexZ005 Oct 11, 2026
93b117d
[feat] 41-windows G6+G7: tabbed floating windows drag by touch; drop …
AlexZ005 Oct 11, 2026
fb91047
[feat] 41-windows G24: the Image editor can be a bottom-dock tab
AlexZ005 Oct 11, 2026
d55b1c4
Merge remote-tracking branch 'origin/feat/41-modals' into feat/41-win…
AlexZ005 Oct 11, 2026
2ac0f74
[fix] 41-windows: dock caret + docked Image editor on the 41-modals z…
AlexZ005 Oct 11, 2026
a99845a
[fix] 41-windows: docked Image editor on --z-dock (the 41-modals scal…
AlexZ005 Oct 11, 2026
96fcb69
[feat] 41-windows G22: the desktop dock "+" lists every window the ph…
AlexZ005 Oct 11, 2026
4669ecb
[test] 41-modals: suites fit the runner — split settings-narrow (480 …
AlexZ005 Oct 11, 2026
ad3710d
[test] 41-windows dock-strip-drop 2.4: release beside the Controls pi…
AlexZ005 Oct 11, 2026
fb7a594
[test] 41-windows dock-chrome 11.2: the floating row's verb is the G2…
AlexZ005 Oct 11, 2026
c53e656
[test] 41-windows dock-tab-drag 7.1: move the re-floated window a cle…
AlexZ005 Oct 11, 2026
917f621
[test] 41-modals: mobile-map finds the copy-invite button as the pill…
AlexZ005 Oct 11, 2026
d11ba9f
Merge remote-tracking branch 'origin/feat/41-modals' into feat/41-win…
AlexZ005 Oct 11, 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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ jobs:
# `tokens-ok: <reason>` on its line; scripts/check-tokens.cjs says how.
- name: redesign tokens
run: node scripts/check-tokens.cjs
# 41 G3b: ONE z-index scale (src/styles/ui.css). A page-level literal z-index fails;
# `z-ok: <reason>` on its line for a number code reads back.
- name: z-index scale
run: node scripts/check-zindex.cjs

# 34 B3: scene-lint over every scene core ships (the seed) and every scene of the scenes ref
# the perf budget is about (perf/budgets.json), with the packs checkout for the kit rules.
Expand Down
19 changes: 11 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4611,14 +4611,17 @@ loadable play content. Everything a user does must be visible to connected peers
- The camera PiP frame deliberately sits BELOW the tiers (z-index 2): it is a
viewport overlay whose picture is drawn by the render loop, so panels and HUD must
cover it (16-Q6).
- **z-index tiers** (`ui.css` `:root`): viewport 0 · drawer 30 · bottom 35 · window 40 ·
hud 45 · **modal 1100 · toast 1200 · menu 1300**. The high modal/toast/menu values
clear the ad-hoc persistent chrome that lives OUTSIDE the scale — Users (avatar/peers)
~996-998, Connect 300, ContextMenu 999-1001, ThemedSelect 9999. flowbite Modal
hardcodes its dialog z-50 + backdrop z-40, remapped onto `--z-modal` by an UNLAYERED
`[role='dialog'][aria-modal='true']` rule (unlayered beats Tailwind's layered utility
without !important). The logo/burger menu sits at `--z-menu` (top-most); opening a
modal from it calls `closeMenu.set(true)` so the menu can't cover the modal.
- **z-index: ONE SCALE (41 G3b), `ui.css` `:root`, lowest first** — `--z-canvas` 0 · `--z-canvas-overlay` 2 ·
`--z-chrome` 30 (FABs, Controls pill, side drawers) · `--z-selection` 32 (phone strip, under every sheet) ·
`--z-dock` 35 · `--z-sheet` 38 · `--z-window` 40..44 · `--z-phone-bar` 44 · `--z-hud` 45 · `--z-side-panel` 50 ·
`--z-chrome-top` 300 (logo, undo/redo, Connect, the top-right corner) · `--z-popover` 1000 (main menu, profile,
Add, search) · `--z-toast-low` 1050 · `--z-modal` 1100 · `--z-onboarding` 1150 (What's new, Welcome, tours) ·
`--z-portal` 1180 (selects, context menus, drag ghosts — above the modals that open them) · `--z-toast` 1200
(approvals). Modals COVER the logo (the old logo-above-modals one-click trick is gone). Use `var(--z-…)` /
`z-(--z-…)` or `calc(var(--z-…) ± n)`; `npm run check:zindex` (CI) fails a page-level literal ≥ 30, a
number code reads back carries `// z-ok: <reason>`. Old names (`--z-drawer/-bottom/-viewport/-menu`) are
aliases. The camera PiP is `--z-canvas-overlay`. App modals are non-modal `<dialog>`s on `--z-modal`;
`ConfirmModal`/`SessionPrompts` are true modals (top layer).
- **flowbite 1.x Dropdowns are TOP-LAYER popovers** (`popover="manual"`, `:popover-open`)
— they paint above the ENTIRE page whatever the z-index (measured: a panel at 996
covered the profile avatar at 2000, in the same stacking context). No z-index on an
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"sync-llms": "node scripts/sync-llms.cjs",
"test:unit": "vitest run",
"check:storage": "node scripts/check-storage.cjs",
"check:tokens": "node scripts/check-tokens.cjs"
"check:tokens": "node scripts/check-tokens.cjs",
"check:zindex": "node scripts/check-zindex.cjs"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
Expand Down
105 changes: 105 additions & 0 deletions scripts/check-zindex.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env node
// 41-modals G3b — `npm run check:zindex`: every PAGE-level z-index comes from the scale in
// src/styles/ui.css (`--z-canvas` … `--z-toast`), never a raw number.
//
// What counts as a violation (src/components, src/routes, src/styles, src/modules, src/lib):
// - a CSS / inline-style / JS-string `z-index: N` with N >= 30
// - a JS `zIndex = 'N'` / `zIndex: N` / `String(N …)` with N >= 30
// - a Tailwind utility `z-N` / `z-[N]` (any variant prefix) with N >= 30
// A number BELOW 30 is stacking inside a component's own stacking context (a resize grip over its
// window's tab strip), which the page scale never sees — the lowest page tier (--z-chrome) is 30.
// Comments are stripped first. A value that must stay numeric (code reads it back as a number) says
// so on its line: `// z-ok: <reason>` (the reason is required).
//
// node scripts/check-zindex.cjs report per file, exit 1 on any violation
// node scripts/check-zindex.cjs --json { total, files: {rel: [{line, match}]} }

const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const SCOPE = ['src/components', 'src/routes', 'src/styles', 'src/modules', 'src/lib'].map((d) => path.join(ROOT, d));
const EXT = /\.(svelte|css|js|ts)$/;
const LOCAL_MAX = 29;

const RULES = [
/z-index\s*:\s*(-?\d+)/g,
/zIndex\s*[:=]\s*['"`]?(-?\d+)/g,
/zIndex\s*=\s*String\(\s*(\d+)/g,
/(?<![\w-])(?:[a-z0-9-]+:)*-?z-\[?(\d+)\]?(?![\w-])/g
];

/**
* Blank out comments, keeping line numbers. @param {string} text @returns {string}
*/
function stripComments(text) {
const blank = (/** @type {string} */ m) => m.replace(/[^\n]/g, ' ');
return text
.replace(/<!--[\s\S]*?-->/g, blank)
.replace(/\/\*[\s\S]*?\*\//g, blank)
.replace(/(^|[\s;{}])\/\/[^\n]*/g, (m, lead) => lead + blank(m.slice(lead.length)));
}

const PRAGMA = /z-ok\s*:\s*\S.{2,}/;

/**
* @param {string} text @returns {{line: number, match: string}[]}
*/
function scanText(text) {
/** @type {{line: number, match: string}[]} */
const out = [];
const raw = text.split('\n');
stripComments(text)
.split('\n')
.forEach((line, i) => {
if (PRAGMA.test(raw[i])) return;
for (const re of RULES) {
re.lastIndex = 0;
let m;
while ((m = re.exec(line))) {
if (Number(m[1]) > LOCAL_MAX) out.push({ line: i + 1, match: m[0].trim() });
}
}
});
return out;
}

/** @param {string} dir @param {string[]} acc @returns {string[]} */
function walk(dir, acc) {
if (!fs.existsSync(dir)) return acc;
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walk(p, acc);
else if (EXT.test(ent.name)) acc.push(p);
}
return acc;
}

if (require.main === module) {
const json = process.argv.includes('--json');
/** @type {Record<string, {line: number, match: string}[]>} */
const files = {};
let total = 0;
for (const dir of SCOPE) {
for (const file of walk(dir, [])) {
const hits = scanText(fs.readFileSync(file, 'utf8'));
if (!hits.length) continue;
files[path.relative(ROOT, file)] = hits;
total += hits.length;
}
}
if (json) {
console.log(JSON.stringify({ total, files }, null, 1));
} else {
for (const [rel, hits] of Object.entries(files)) for (const h of hits) console.log(`${rel}:${h.line} ${h.match}`);
console.log(
total
? `\n${total} page-level z-index literal(s). Use a token from the scale in src/styles/ui.css ` +
'(var(--z-…) / z-(--z-…), or calc(var(--z-…) ± n)); a number the code must read back says why with `z-ok: <reason>`.'
: 'check:zindex — every page-level z-index is on the scale.'
);
}
process.exit(total ? 1 : 0);
}

module.exports = { scanText, LOCAL_MAX };
2 changes: 1 addition & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ import { startMusicToolbox } from './lib/musicToolbox'
`bottom` is the open dock's height, which is what makes the dock a REGION (the
DCC behaviour: the canvas ends where the panel begins) instead of an overlay
drawn on top of a full-window canvas. No `z-index` — a stacking context here
would trap the PiP frame and the framing guide; the dock's --z-bottom (35)
would trap the PiP frame and the framing guide; the dock's --z-dock (35)
already sits above the canvas at the default 0. No `transition` either: each
step reallocates the composer's render targets, so animating the inset turns one
realloc into one per frame. -->
Expand Down
4 changes: 2 additions & 2 deletions src/components/ContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@
<div
use:portal
class="fixed inset-0"
style="z-index: var(--z-menu);"
style="z-index: var(--z-portal);"
role="presentation"
on:pointerdown={() => (backdropPressed = true)}
on:click={() => backdropPressed && dispatch('close')}
Expand All @@ -433,7 +433,7 @@
use:place
use:minimalScroll
class="ctx-scroll tp-ui tp-menu fixed min-w-36 overflow-y-auto overflow-x-hidden"
style="left: 0; top: 0; z-index: calc(var(--z-menu) + 1);"
style="left: 0; top: 0; z-index: calc(var(--z-portal) + 1);"
role="menu"
on:mousedown={keepFocus}
on:pointerdown|capture={() => (menuPressed = true)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContextMenuItems.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
use:minimalScroll
on:mouseenter={() => clearTimeout(closeTimer)}
class="ctx-scroll tp-ui tp-menu fixed min-w-36 overflow-y-auto overflow-x-hidden"
style="z-index: calc(var(--z-menu) + 2);"
style="z-index: calc(var(--z-portal) + 2);"
>
<!-- 38 R9: on the phone shell a submenu is a sheet laid OVER its parent
(drill in place), so it needs a way back; hidden everywhere else
Expand Down
8 changes: 5 additions & 3 deletions src/components/Flow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@
// another dock view. They start docked, so they appear as dock tabs. Same list the
// strip's "+" renders ($lib/dockMenu) — they used to be two copies that drifted.
let addMenu: { x: number; y: number } | null = $state(null);
const addItems = dockAddItems();
// 41 G22: built per open (it reads what is docked right now) and without this window's own row
let addItems: any[] = $state([]);
function openAddMenu(e: MouseEvent) {
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
addItems = dockAddItems({ exclude: ['flow'] });
addMenu = { x: r.left, y: r.bottom + 4 };
}

Expand Down Expand Up @@ -186,14 +188,14 @@
id="flow-list"
transition:fly={{ y: 320, duration: 200 }}
class="tp-ui tp-dock-panel fixed inset-x-0 bottom-0 p-2 {dockVisible ? '' : 'hidden'}"
style="z-index: var(--z-bottom); height: {$dockHeight}px"
style="z-index: var(--z-dock); height: {$dockHeight}px"
data-key-scope="panel"
role="region"
aria-label="Node editor (docked)"
>
<!-- top-edge resize hot zone -->
<div
class="resize-cue absolute -top-1 left-0 right-0 z-30 h-2 cursor-ns-resize hover:bg-accent/30"
class="resize-cue absolute -top-1 left-0 right-0 z-28 h-2 cursor-ns-resize hover:bg-accent/30"
style="touch-action: none"
title="Drag to resize"
onpointerdown={startResize}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import MeshGenModal from './menu/MeshGenModal.svelte';
import MeshJobsCard from './menu/MeshJobsCard.svelte';
import Toasts from './menu/Toasts.svelte';
import SessionPrompts from './menu/SessionPrompts.svelte';
import FramingGuide from './menu/FramingGuide.svelte';
import CameraPipWindow from './menu/CameraPipWindow.svelte';
import Connect from './menu/Connect.svelte';
Expand Down Expand Up @@ -82,6 +83,7 @@
<NotesDrawer />
<LayoutsMenu />
<Toasts />
<SessionPrompts />
<Users />
<!-- 16-P5: letterbox bars while previewing a camera at a fixed aspect -->
<FramingGuide />
Expand Down
4 changes: 2 additions & 2 deletions src/components/editors/AnimationWindow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2681,13 +2681,13 @@
id="animation-dock"
use:noNativeMenu
class="fixed inset-x-0 bottom-0 tp-ui tp-dock-panel flex flex-col p-2 {dockVisible ? '' : 'hidden'}"
style="z-index: var(--z-bottom); height: {$dockHeight}px"
style="z-index: var(--z-dock); height: {$dockHeight}px"
data-key-scope="panel"
role="region"
aria-label="Animation (docked)"
>
<div
class="resize-cue absolute -top-1 left-0 right-0 z-30 h-2 cursor-ns-resize hover:bg-accent/30"
class="resize-cue absolute -top-1 left-0 right-0 z-28 h-2 cursor-ns-resize hover:bg-accent/30"
style="touch-action: none"
title="Drag to resize"
onpointerdown={startResize}
Expand Down
4 changes: 2 additions & 2 deletions src/components/editors/CodeQuickPick.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@
.qp-backdrop {
position: absolute;
inset: 0;
z-index: 40;
z-index: 18;
}
.qp {
position: absolute;
top: 6px;
left: 50%;
z-index: 41;
z-index: 19;
display: flex;
flex-direction: column;
width: min(520px, calc(100% - 24px));
Expand Down
6 changes: 3 additions & 3 deletions src/components/editors/CodeWorkspace.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@
/>
{/if}
{#if confirmCloseAll}
<div id="code-ws-confirm-all" class="absolute inset-0 z-50 flex items-center justify-center bg-scrim" role="dialog" aria-modal="true" aria-label="Unsaved changes">
<div id="code-ws-confirm-all" class="absolute inset-0 z-29 flex items-center justify-center bg-scrim" role="dialog" aria-modal="true" aria-label="Unsaved changes">
<div class="ui-panel w-96 rounded-lg p-4 text-sm shadow-2xl">
<p class="mb-2 font-semibold">{dirtyTabs().length === 1 ? '1 file has' : dirtyTabs().length + ' files have'} unsaved changes</p>
<ul class="code-muted mb-3 max-h-24 overflow-auto text-xs" use:minimalScroll>
Expand Down Expand Up @@ -744,15 +744,15 @@
id="code-ws-dock"
data-tour="code-workspace"
class="code-ws tp-ui tp-dock-panel fixed inset-x-0 bottom-0 flex flex-col p-2 {dockVisible ? '' : 'hidden'}"
style="z-index: var(--z-bottom); height: {$dockHeight}px"
style="z-index: var(--z-dock); height: {$dockHeight}px"
data-key-scope="panel"
role="region"
aria-label="Code (docked)"
use:keys
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="resize-cue absolute -top-1 left-0 right-0 z-30 h-2 cursor-ns-resize hover:bg-accent/30"
class="resize-cue absolute -top-1 left-0 right-0 z-28 h-2 cursor-ns-resize hover:bg-accent/30"
style="touch-action: none"
title="Drag to resize"
onpointerdown={startResize}
Expand Down
33 changes: 19 additions & 14 deletions src/components/editors/Explorer.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { onLayoutRestore, storedPanelLayout } from '$lib/uiLayoutsCore';
import { showChoice, showConfirm } from '$lib/confirmDialog.js';
import Icon from '../ui/Icon.svelte';
import EmptyState from '../ui/EmptyState.svelte';
// Explorer (95, tree v2 in 106): dockable asset browser — real file-manager
Expand Down Expand Up @@ -4286,13 +4287,13 @@
return showToast('Select the object (or objects) to save into this prefab first');
// the opt-in prompt for people who want to be asked (default OFF)
if ($confirmPrefabUpdate)
return showToast(
`Replace "${prefab.name}" with ${uuids.length === 1 ? 'the selected object' : uuids.length + ' selected objects'}?`,
[
{ label: 'Update', action: () => void applyPrefabUpdate(prefab, uuids) },
{ label: 'Cancel', action: () => {} }
]
);
return void showChoice({
title: 'Update this prefab?',
message: `Replace "${prefab.name}" with ${uuids.length === 1 ? 'the selected object' : uuids.length + ' selected objects'}?`,
choices: [{ value: 'update', label: 'Update' }]
}).then((answer) => {
if (answer === 'update') void applyPrefabUpdate(prefab, uuids);
});
void applyPrefabUpdate(prefab, uuids);
}

Expand Down Expand Up @@ -4327,10 +4328,14 @@
}

function confirmDeletePrefab(prefab: any) {
showToast(`Delete the prefab "${prefab.name}"?`, [
{ label: 'Delete', action: () => void removePrefab(prefab.id) },
{ label: 'Cancel', action: () => {} }
]);
// 41 G16: a destructive question is a modal, not a toast
void showConfirm({
title: 'Delete this prefab?',
message: `"${prefab.name}" will be removed from your prefab library.`,
confirmLabel: 'Delete'
}).then((ok) => {
if (ok) void removePrefab(prefab.id);
});
}

/** 37 R4: "Move to folder ▸" — every folder of the tab, the root first, and a new one.
Expand Down Expand Up @@ -8337,7 +8342,7 @@
id="explorer-list"
transition:fly={{ y: 300, duration: 200 }}
class="tp-ui tp-dock-panel fixed inset-x-0 bottom-0 p-2 {dockVisible ? '' : 'hidden'}"
style="z-index: var(--z-bottom); height: {$dockHeight}px"
style="z-index: var(--z-dock); height: {$dockHeight}px"
data-key-scope="panel"
aria-label="Explorer (docked)"
ondragover={(e) => {
Expand All @@ -8350,7 +8355,7 @@
role="region"
>
<div
class="resize-cue absolute -top-1 left-0 right-0 z-30 h-2 cursor-ns-resize hover:bg-accent/30"
class="resize-cue absolute -top-1 left-0 right-0 z-28 h-2 cursor-ns-resize hover:bg-accent/30"
style="touch-action: none"
title="Drag to resize"
onpointerdown={startResize}
Expand Down Expand Up @@ -8489,7 +8494,7 @@
{#if tDragging && tDrag}
<div
id="explorer-touch-ghost"
class="pointer-events-none fixed z-1400 max-w-[160px] -translate-x-1/2 -translate-y-1/2 truncate rounded-sm border border-accent bg-surface-1 px-2 py-1 text-center text-xs font-semibold text-text shadow-lg"
class="pointer-events-none fixed z-(--z-portal) max-w-[160px] -translate-x-1/2 -translate-y-1/2 truncate rounded-sm border border-accent bg-surface-1 px-2 py-1 text-center text-xs font-semibold text-text shadow-lg"
style="left: {tGhostX}px; top: {tGhostY}px;"
>
{tDrag.label}
Expand Down
4 changes: 2 additions & 2 deletions src/components/editors/FlowCode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,13 @@
<div
id="flow-code-dock"
class="fixed inset-x-0 bottom-0 tp-ui tp-dock-panel flex flex-col p-2 {dockVisible ? '' : 'hidden'}"
style="z-index: var(--z-bottom); height: {$dockHeight}px"
style="z-index: var(--z-dock); height: {$dockHeight}px"
data-key-scope="panel"
role="region"
aria-label="Flow Code (docked)"
>
<div
class="resize-cue absolute -top-1 left-0 right-0 z-30 h-2 cursor-ns-resize hover:bg-accent/30"
class="resize-cue absolute -top-1 left-0 right-0 z-28 h-2 cursor-ns-resize hover:bg-accent/30"
style="touch-action: none"
title="Drag to resize"
onpointerdown={startResize}
Expand Down
Loading
Loading