Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,19 +439,17 @@ 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):

| Element | z-index | File |
|---------------------------------------|---------|------|
| 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` |
Expand Down Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions docs/reference/design-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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.

Expand Down
34 changes: 34 additions & 0 deletions src/__tests__/site-explorer/siteExplorerPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SiteExplorerPanel sectionGroup="site" />)
Expand Down
12 changes: 4 additions & 8 deletions src/admin/pages/site/canvas/CanvasRoot.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/admin/pages/site/panels/AgentPanel/AgentPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down
Loading