diff --git a/docs/editor.md b/docs/editor.md index f10e1d0bf..0f2177e20 100644 --- a/docs/editor.md +++ b/docs/editor.md @@ -439,7 +439,7 @@ Full details: [`docs/features/canvas-iframe-per-frame.md`](features/canvas-ifram `CanvasRoot` (`src/admin/pages/site/canvas/CanvasRoot.module.css`) sets `position: relative; z-index: 0`. This establishes an **isolating stacking context** for the entire canvas subtree. Every canvas-internal z-index value is confined inside that context and cannot compete with sibling layout elements. -Why this matters: selection rings and the floating selection toolbar are portaled into the canvas root and painted at z-index 51 (above the `PluginCanvasOverlayLayer` at 50). Without the `z-index: 0` stacking context on the canvas, those z-index 51 values escape into the shared layout context and paint over the floating `PropertiesPanel` (also z-index 50), which is a sibling of the canvas. With the isolation in place, the canvas as a whole occupies z-index 0 in the shared layout context — well below the panel's 50. +Why this matters: selection rings and the floating selection toolbar are portaled into the canvas root and painted at z-index 51 (above the `PluginCanvasOverlayLayer` at 50). Without the `z-index: 0` stacking context on the canvas, those values escape into the shared layout context instead of remaining a single isolated canvas layer. With the isolation in place, the canvas as a whole occupies z-index 0 in the shared layout context — well below the floating-panel tier at 90. **Editor layout z-index table** (shared context, outside the canvas): @@ -447,11 +447,9 @@ Why this matters: selection rings and the floating selection toolbar are portale |---------------------------------------|---------|------| | Canvas (CanvasRoot, isolation root) | 0 | `canvas/CanvasRoot.module.css` | | Toolbar (main bar) | 30 | `toolbar/Toolbar.module.css` | -| PropertiesPanel (floating) | 50 | `panels/PropertiesPanel/PropertiesPanel.module.css` | -| AgentPanel (floating) | 50 | `panels/AgentPanel/AgentPanel.module.css` | | PanelRail | 55 | `sidebars/PanelRail/PanelRail.module.css` | -| LeftSidebar, RightSidebar | 85 | `sidebars/{Left,Right}Sidebar/` | -| Undocked left-panel host | 90 | `sidebars/LeftSidebar/LeftSidebar.module.css` | +| Docked left-panel host, RightSidebar | 85 | `sidebars/{Left,Right}Sidebar/` | +| PropertiesPanel, AgentPanel, undocked left-panel host, shared floating windows | 90 | `panels/`, `sidebars/LeftSidebar/`, `shared/FloatingWindow/` | | CodeEditorPanel (floats over sidebars)| 95 | `code-editor/CodeEditorPanel.module.css` | | Toolbar popovers / dropdowns | 201 | `toolbar/Toolbar.module.css` | | PreviewOverlay | 400–401 | `preview/PreviewOverlay.module.css` | @@ -544,6 +542,11 @@ floating window in both axes (arrow keys resize by 10px; Shift+arrow by 40px). height are persisted through `siteEditorLayoutPersistence` / `workspaceLayoutStorage`. +The outer left-sidebar layout shell intentionally has no `z-index`, so it does +not trap floating descendants in a sidebar stacking context. Its docked panel +slot owns layer 85; the undocked slot and independent Agent panel participate +directly in the shared floating tier at 90. + The AI Assistant is an independent draggable and resizable floating window, so it can stay open beside Explorer/Layers or any other hosted panel. Its position, dimensions, and open state persist separately across reloads. Properties diff --git a/docs/reference/design-tokens.md b/docs/reference/design-tokens.md index 856b98e5a..a87948ed4 100644 --- a/docs/reference/design-tokens.md +++ b/docs/reference/design-tokens.md @@ -416,10 +416,10 @@ The visual editor uses additional raw z-index values that are **not** tokenised. |-------|-----------------| | 0 | `CanvasRoot` — an isolation root; all canvas-internal values are confined here | | 30 | Main toolbar | -| 50 | Floating panels: PropertiesPanel, AgentPanel, DomPanel | -| 55 | LeftSidebar, RightSidebar, PanelRail | -| 90 | Shared admin floating windows (`FloatingWindow`, `MediaViewerWindow`, agent image preview) | -| 80 | CodeEditorPanel | +| 55 | PanelRail | +| 85 | Docked left-panel host, RightSidebar | +| 90 | PropertiesPanel, AgentPanel, undocked left-panel host, shared admin floating windows | +| 95 | CodeEditorPanel | | 201 | Toolbar popovers / dropdowns | | 400–401 | PreviewOverlay | @@ -433,7 +433,11 @@ The visual editor uses additional raw z-index values that are **not** tokenised. | 60 | CanvasContextSelector | | 2147483647 | Drop-indicator layer inside iframe (must beat arbitrary module stacking contexts) | -`CanvasRoot` declares `z-index: 0; position: relative` to establish the isolation. Without it, the canvas-internal z-index 51 would escape into the layout context and paint over floating panels at z-index 50. See [`docs/editor.md`](../editor.md) → "Canvas stacking context isolation" for the full explanation. +`CanvasRoot` declares `z-index: 0; position: relative` to establish the isolation. Without it, canvas-internal values would escape into the shared layout context instead of remaining one canvas layer beneath the floating-panel tier at 90. See [`docs/editor.md`](../editor.md) → "Canvas stacking context isolation" for the full explanation. + +The left-sidebar layout shell deliberately does not create a stacking context. +Its docked panel slot owns layer 85, which lets the Agent panel and the shared +undocked panel host participate directly in the editor-wide layer 90. Raw canvas-internal values are intentional exceptions — they cannot be tokens because they are relative to an isolated stacking context, not the global one. Do not add new raw z-index values outside this established ladder. diff --git a/src/__tests__/site-explorer/siteExplorerPanel.test.tsx b/src/__tests__/site-explorer/siteExplorerPanel.test.tsx index 5a25d89fa..237c0cdef 100644 --- a/src/__tests__/site-explorer/siteExplorerPanel.test.tsx +++ b/src/__tests__/site-explorer/siteExplorerPanel.test.tsx @@ -878,6 +878,40 @@ describe('SiteExplorerPanel', () => { expect(panelZIndex).toBeGreaterThan(Math.max(...sidebarZIndexes)) }) + it('keeps every floating editor panel above the docked panel hosts', () => { + const leftSidebarCss = readFileSync( + new URL('../../admin/pages/site/sidebars/LeftSidebar/LeftSidebar.module.css', import.meta.url), + 'utf-8', + ) + const rightSidebarCss = readFileSync( + new URL('../../admin/pages/site/sidebars/RightSidebar/RightSidebar.module.css', import.meta.url), + 'utf-8', + ) + const agentPanelCss = readFileSync( + new URL('../../admin/pages/site/panels/AgentPanel/AgentPanel.module.css', import.meta.url), + 'utf-8', + ) + const propertiesPanelCss = readFileSync( + new URL('../../admin/pages/site/panels/PropertiesPanel/PropertiesPanel.module.css', import.meta.url), + 'utf-8', + ) + + const sidebarRule = leftSidebarCss.match(/\.sidebar\s*\{[\s\S]*?\}/)?.[0] ?? '' + const dockedSlotRule = leftSidebarCss.match(/\.panelSlot\s*\{[\s\S]*?\}/)?.[0] ?? '' + const rightSidebarRule = rightSidebarCss.match(/\.sidebar\s*\{[\s\S]*?\}/)?.[0] ?? '' + const floatingSlotRule = leftSidebarCss.match(/\.panelSlotFloating\s*\{[\s\S]*?\}/)?.[0] ?? '' + const agentRule = agentPanelCss.match(/\.floatPanel\s*\{[\s\S]*?\}/)?.[0] ?? '' + const propertiesRule = propertiesPanelCss.match(/\.panel\s*\{[\s\S]*?\}/)?.[0] ?? '' + const dockedZIndexes = [dockedSlotRule, rightSidebarRule] + .map((rule) => Number(rule.match(/z-index:\s*(\d+)/)?.[1])) + const floatingZIndexes = [floatingSlotRule, agentRule, propertiesRule] + .map((rule) => Number(rule.match(/z-index:\s*(\d+)/)?.[1])) + + expect(sidebarRule).not.toContain('z-index:') + expect(dockedZIndexes).toEqual([85, 85]) + expect(floatingZIndexes.every((zIndex) => zIndex > Math.max(...dockedZIndexes))).toBe(true) + }) + it('opens pages and components on the canvas from concept rows', () => { loadSite() render() diff --git a/src/admin/pages/site/canvas/CanvasRoot.module.css b/src/admin/pages/site/canvas/CanvasRoot.module.css index d0d19c5d2..565c278de 100644 --- a/src/admin/pages/site/canvas/CanvasRoot.module.css +++ b/src/admin/pages/site/canvas/CanvasRoot.module.css @@ -6,14 +6,10 @@ .canvas { flex: 1; position: relative; - /* Establish a stacking context for the whole canvas subtree. The selection / - hover rings and toolbar are portaled into this element at z-index 51 (so - they paint above the in-canvas PluginCanvasOverlayLayer at 50). Without an - isolating context here, that 51 escapes into the shared layout stacking - context and paints over the floating PropertiesPanel (z-index 50), which - is a sibling of the canvas. Pinning the canvas to z-index 0 keeps the - rings' internal ordering intact while ensuring the entire canvas — rings - included — sits below floating overlay panels and sidebars. */ + /* Establish a stacking context for the whole canvas subtree. Canvas chrome + owns an internal z-index ladder for selection rings, mode controls, and + plugin overlays. Pinning the canvas to z-index 0 keeps that ordering + internal while ensuring the entire canvas sits below editor panels. */ z-index: 0; overflow: hidden; background: var(--bg-surface-2); diff --git a/src/admin/pages/site/panels/AgentPanel/AgentPanel.module.css b/src/admin/pages/site/panels/AgentPanel/AgentPanel.module.css index 318423d45..8bc45f60b 100644 --- a/src/admin/pages/site/panels/AgentPanel/AgentPanel.module.css +++ b/src/admin/pages/site/panels/AgentPanel/AgentPanel.module.css @@ -10,7 +10,10 @@ --panel-h: 480px; left: var(--panel-x); top: var(--panel-y); - z-index: 50; + /* Shared floating tier above docked panel slots (85). The left sidebar + layout shell deliberately does not create a stacking context, so this + panel participates in the editor-wide layer. */ + z-index: 90; width: var(--panel-w); height: var(--panel-h); min-width: 280px; diff --git a/src/admin/pages/site/panels/PropertiesPanel/PropertiesPanel.module.css b/src/admin/pages/site/panels/PropertiesPanel/PropertiesPanel.module.css index 5f846ed25..7205a4959 100644 --- a/src/admin/pages/site/panels/PropertiesPanel/PropertiesPanel.module.css +++ b/src/admin/pages/site/panels/PropertiesPanel/PropertiesPanel.module.css @@ -12,7 +12,9 @@ left: var(--panel-x); top: var(--panel-y); bottom: auto; - z-index: 50; + /* Shared floating tier above docked panel slots (85). The docked variant + resets to auto below. */ + z-index: 90; display: flex; flex-direction: column; width: var(--panel-w); diff --git a/src/admin/pages/site/sidebars/LeftSidebar/LeftSidebar.module.css b/src/admin/pages/site/sidebars/LeftSidebar/LeftSidebar.module.css index dec266937..cb6593266 100644 --- a/src/admin/pages/site/sidebars/LeftSidebar/LeftSidebar.module.css +++ b/src/admin/pages/site/sidebars/LeftSidebar/LeftSidebar.module.css @@ -1,6 +1,9 @@ .sidebar { position: relative; - z-index: 85; + /* Keep the layout shell out of the stacking ladder. Floating descendants + (the Agent panel and an undocked panel slot) must participate in the + editor's shared z-index context instead of being trapped below the + later right sidebar. The docked panel owns layer 85 below. */ display: flex; --left-sidebar-rail-width: 42px; --left-sidebar-panel-width: 0px; @@ -26,6 +29,7 @@ .panelSlot { --panel-fade-bg: var(--bg-body); position: absolute; + z-index: 85; top: 0; bottom: 0; left: var(--left-sidebar-rail-width);