diff --git a/design-system/packages/design-tokens/README.md b/design-system/packages/design-tokens/README.md index 12b61448fb..6ca987c9cc 100644 --- a/design-system/packages/design-tokens/README.md +++ b/design-system/packages/design-tokens/README.md @@ -23,6 +23,15 @@ line travel and intrinsic-width changes. It has its own token so tuning content replacement does not change the existing hover-feedback and larger-view transition durations. It is zeroed in the reduced-motion projection. +## Overlay layers + +Portaled tooltips must paint above the popovers containing their triggers. +Use `layer.popover` for menus and panels, with adjacent offsets for their +backdrops (-1) and nested menus (+1); keep those offsets below `layer.tooltip`. +Toast, notification, and context-menu layers remain above ordinary hover hints. +These values order siblings within a stacking context; a portal host still +needs its own layer above the app content it serves. + ## Semantic typography roles Text-bearing components consume a complete semantic role rather than assembling diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json index c02306ed89..65f311664d 100644 --- a/design-system/packages/design-tokens/src/system.tokens.json +++ b/design-system/packages/design-tokens/src/system.tokens.json @@ -989,7 +989,7 @@ "modalActive": { "$value": 250 }, "fullscreen": { "$value": 280 }, "overlayHost": { "$value": 300 }, - "tooltip": { "$value": 350 }, + "tooltip": { "$value": 370 }, "popover": { "$value": 360 }, "toast": { "$value": 400 }, "notification": { "$value": 400 }, diff --git a/design-system/packages/design-tokens/tests/contract.test.mjs b/design-system/packages/design-tokens/tests/contract.test.mjs index 314210cd73..1160895fb3 100644 --- a/design-system/packages/design-tokens/tests/contract.test.mjs +++ b/design-system/packages/design-tokens/tests/contract.test.mjs @@ -436,6 +436,20 @@ test("shared system scales preserve the migrated Web UI foundation contract", () assert.equal(tokens["layer.contextMenu"], 500); }); +test("tooltips outrank popovers and nested menus without covering priority chrome", () => { + for (const mode of tokenModes) { + const modeTokens = Object.fromEntries( + tokenCatalog.filter(token => token.category === "layer") + .map(token => [token.name, Number(token.values[mode])]), + ); + assert.ok(modeTokens["layer.modal"] < modeTokens["layer.popover"]); + assert.ok(modeTokens["layer.popover"] + 1 < modeTokens["layer.tooltip"]); + for (const layer of ["toast", "notification", "contextMenu"]) { + assert.ok(modeTokens["layer.tooltip"] < modeTokens[`layer.${layer}`]); + } + } +}); + test("semantic typography roles resolve to the canonical foundation", async () => { const systemDocument = await readSource("system.tokens.json"); diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index b40a352906..43d4457d80 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -200,7 +200,7 @@ --openbitfun-layer-popover: 360; --openbitfun-layer-sticky: 15; --openbitfun-layer-toast: 400; - --openbitfun-layer-tooltip: 350; + --openbitfun-layer-tooltip: 370; --openbitfun-layout-card-footer-gap: var(--openbitfun-space-2); --openbitfun-layout-card-gap-lg: 30px; --openbitfun-layout-card-gap-md: var(--openbitfun-space-3); diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index 8141a2216e..ca23b08b01 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -981,7 +981,7 @@ $_section-header-height: 22px; width: fit-content; max-width: min(440px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 16px)); - z-index: 9999; + z-index: var(--openbitfun-layer-popover); transform-origin: top left; animation: openbitfun-footer-menu-in var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate) forwards; @@ -1028,6 +1028,8 @@ $_section-header-height: 22px; } &__workspace-menu-workspaces { + display: flex; + flex-direction: column; max-height: 240px; overscroll-behavior: contain; } @@ -1604,7 +1606,7 @@ $_section-header-height: 22px; .openbitfun-nav-panel__footer-backdrop { position: fixed; inset: 0; - z-index: 9998; + z-index: calc(var(--openbitfun-layer-popover) - 1); } /* Positioning-only overrides; the design-system Menu owns the surface look. */ @@ -1613,7 +1615,7 @@ $_section-header-height: 22px; min-width: 148px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); - z-index: 9999; + z-index: var(--openbitfun-layer-popover); transform-origin: bottom right; animation: openbitfun-footer-menu-in var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate) forwards; @@ -1640,7 +1642,7 @@ $_section-header-height: 22px; position: fixed; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); - z-index: 10000; + z-index: calc(var(--openbitfun-layer-popover) + 1); animation: openbitfun-nav-popover-in 150ms cubic-bezier(0.23, 1, 0.32, 1) both; &[data-placement='right'] { @@ -1658,7 +1660,7 @@ $_section-header-height: 22px; .openbitfun-device-overview { position: fixed; - z-index: 9999; + z-index: var(--openbitfun-layer-popover); width: min(280px, calc(100vw - 16px)); max-height: calc(100vh - 16px); display: flex; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index 609218e64b..68c3515185 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -460,7 +460,7 @@ &__inline-item-menu-popover { position: fixed; - z-index: 10000; + z-index: var(--openbitfun-layer-popover); } &__inline-item-edit { diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss index e808331bfb..d9fde898c5 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss @@ -910,7 +910,7 @@ min-inline-size: 180px; max-inline-size: min(320px, calc(100vw - 24px)); max-block-size: min(560px, calc(100vh - 16px)); - z-index: 10000; + z-index: var(--openbitfun-layer-popover); transform-origin: top left; animation: openbitfun-footer-menu-in var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate) forwards; } diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss index b98d662a06..ef4bc22744 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss @@ -83,7 +83,7 @@ --openbitfun-overlay-menu-inline-size: min(280px, calc(100vw - 16px)); --openbitfun-overlay-menu-max-block-size: min(360px, calc(100vh - 16px)); min-width: 200px; - z-index: 99999; + z-index: var(--openbitfun-layer-popover); animation: menuSlideIn 0.12s ease-out; @keyframes menuSlideIn { diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss index 777016e68c..c195b0b864 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss @@ -467,7 +467,7 @@ &__background-command-menu { position: fixed; - z-index: 10000; + z-index: calc(var(--openbitfun-layer-popover) + 1); &--portal { top: auto; diff --git a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.scss b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.scss index a604bd6ec3..3c2902576e 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.scss +++ b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.scss @@ -250,7 +250,7 @@ position: fixed; top: auto; right: auto; - z-index: 10000; + z-index: calc(var(--openbitfun-layer-popover) + 1); } &__node-copy { diff --git a/src/web-ui/src/infrastructure/appearance/runtime/AppearanceOverlayHost.test.ts b/src/web-ui/src/infrastructure/appearance/runtime/AppearanceOverlayHost.test.ts index d2e11a9d4a..07df025a7c 100644 --- a/src/web-ui/src/infrastructure/appearance/runtime/AppearanceOverlayHost.test.ts +++ b/src/web-ui/src/infrastructure/appearance/runtime/AppearanceOverlayHost.test.ts @@ -53,6 +53,26 @@ describe('appearance overlay host', () => { expect(systemTokens.layer.overlayHost.$value).toBeLessThan(systemTokens.layer.contextMenu.$value); }); + it.each([ + 'src/app/components/NavPanel/NavPanel.scss', + 'src/app/components/NavPanel/sections/sessions/SessionsSection.scss', + 'src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss', + 'src/app/components/panels/content-canvas/tab-bar/TabOverflowMenu.scss', + 'src/flow_chat/components/modern/FlowChatHeader.scss', + 'src/flow_chat/components/modern/SessionTreePopover.scss', + 'src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss', + ])('keeps %s below portaled tooltips', (path) => { + const styles = readSource(path); + // Legacy 9999/10000 menu layers bypassed the shared scale and covered + // Tooltip/OverflowText siblings even after the portal escaped clipping. + for (const [, value] of styles.matchAll(/z-index:\s*(\d+)\s*;/g)) { + expect(Number(value), `${path}: z-index ${value}`).toBeLessThan( + systemTokens.layer.tooltip.$value, + ); + } + expect(systemTokens.layer.popover.$value + 1).toBeLessThan(systemTokens.layer.tooltip.$value); + }); + // A containing block on the host would re-anchor every `position: fixed` // overlay to the host box instead of the viewport, moving all of them. it('never becomes a containing block for fixed-position overlays', () => { diff --git a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss index 9196c32e74..18b4d6ffb4 100644 --- a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss +++ b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.scss @@ -82,12 +82,12 @@ &__backdrop { position: fixed; inset: 0; - z-index: 9998; + z-index: calc(var(--openbitfun-layer-popover) - 1); } &__menu { position: fixed; - z-index: 9999; + z-index: var(--openbitfun-layer-popover); } &__item-metadata {