From 26fa227576e8be2fcbd179a0a7f665a2c6d15060 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 10:22:52 -0400 Subject: [PATCH 01/10] added z-index soft scale --- packages/eslint-plugin-gamut/src/index.tsx | 2 + .../src/no-raw-z-index.test.ts | 55 +++++ .../eslint-plugin-gamut/src/no-raw-z-index.ts | 62 +++++ .../eslint-plugin-gamut/src/recommended.ts | 1 + .../__snapshots__/theme.test.ts.snap | 222 +++++++++++++++++- packages/gamut-styles/src/themes/core.ts | 3 + .../gamut-styles/src/variables/elements.ts | 9 +- packages/gamut-styles/src/variables/index.ts | 1 + .../gamut-styles/src/variables/zIndices.ts | 43 ++++ packages/gamut-styles/src/variance/config.ts | 3 + packages/gamut/src/Anchor/index.tsx | 9 +- .../gamut/src/BarChart/layout/GridLines.tsx | 4 +- packages/gamut/src/BodyPortal/index.tsx | 16 +- packages/gamut/src/Box/props.ts | 4 +- packages/gamut/src/Button/shared/styles.ts | 3 +- packages/gamut/src/DataList/EmptyRows.tsx | 4 +- .../Calendar/CalendarWrapper.tsx | 3 +- .../Calendar/utils/elements.tsx | 9 +- packages/gamut/src/Flyout/index.tsx | 3 +- .../Form/SelectDropdown/SelectDropdown.tsx | 3 + .../gamut/src/Form/SelectDropdown/styles.ts | 8 +- packages/gamut/src/List/TableHeader.tsx | 9 +- packages/gamut/src/List/elements.tsx | 9 +- packages/gamut/src/Menu/elements.tsx | 7 +- packages/gamut/src/Overlay/index.tsx | 9 +- .../src/PatternBackdrop/PatternBackdrop.tsx | 11 +- packages/gamut/src/Popover/elements.tsx | 6 +- packages/gamut/src/Popover/styles/base.ts | 4 +- packages/gamut/src/Popover/styles/variants.ts | 4 +- .../src/PopoverContainer/PopoverContainer.tsx | 9 +- packages/gamut/src/Tabs/TabButton.tsx | 6 +- packages/gamut/src/Tabs/Tabs.tsx | 6 +- packages/gamut/src/Tabs/styles.tsx | 4 +- .../gamut/src/Tip/PreviewTip/elements.tsx | 4 +- packages/gamut/src/Tip/shared/InlineTip.tsx | 4 +- packages/gamut/src/Tip/shared/elements.tsx | 4 +- packages/gamut/src/Toaster/index.tsx | 4 +- packages/gamut/src/Typography/Text.tsx | 5 +- 38 files changed, 499 insertions(+), 73 deletions(-) create mode 100644 packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts create mode 100644 packages/eslint-plugin-gamut/src/no-raw-z-index.ts create mode 100644 packages/gamut-styles/src/variables/zIndices.ts diff --git a/packages/eslint-plugin-gamut/src/index.tsx b/packages/eslint-plugin-gamut/src/index.tsx index 9459f37a2a0..f4346fbc28d 100644 --- a/packages/eslint-plugin-gamut/src/index.tsx +++ b/packages/eslint-plugin-gamut/src/index.tsx @@ -2,6 +2,7 @@ import gamutImportPaths from './gamut-import-paths'; import noCssStandalone from './no-css-standalone'; import noInlineStyle from './no-inline-style'; import noKbdElement from './no-kbd-element'; +import noRawZIndex from './no-raw-z-index'; import preferThemed from './prefer-themed'; import recommended from './recommended'; @@ -10,6 +11,7 @@ const rules = { 'no-css-standalone': noCssStandalone, 'no-inline-style': noInlineStyle, 'no-kbd-element': noKbdElement, + 'no-raw-z-index': noRawZIndex, 'prefer-themed': preferThemed, }; diff --git a/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts b/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts new file mode 100644 index 00000000000..75da9221e7b --- /dev/null +++ b/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts @@ -0,0 +1,55 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; + +import rule from './no-raw-z-index'; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('no-raw-z-index', rule, { + valid: [ + // Semantic tokens are the expected usage. + `const styles = { zIndex: zIndices.modal };`, + `;`, + // Arithmetic on a token is allowed (e.g. Tip's shadow). + `const styles = { zIndex: zIndices.foreground - 2 };`, + `;`, + // Variables / non-literal expressions are not flagged. + `;`, + `const styles = { zIndex };`, + // Unrelated properties. + `const styles = { padding: 0 };`, + `;`, + ], + invalid: [ + { + code: `const styles = { zIndex: 1 };`, + errors: [{ messageId: 'noRawZIndex' }], + }, + { + code: `const styles = { zIndex: 0 };`, + errors: [{ messageId: 'noRawZIndex' }], + }, + { + code: `const styles = { zIndex: -1 };`, + errors: [{ messageId: 'noRawZIndex' }], + }, + { + code: `const styles = { 'z-index': 100 };`, + errors: [{ messageId: 'noRawZIndex' }], + }, + { + code: `;`, + errors: [{ messageId: 'noRawZIndex' }], + }, + { + code: `;`, + errors: [{ messageId: 'noRawZIndex' }], + }, + ], +}); diff --git a/packages/eslint-plugin-gamut/src/no-raw-z-index.ts b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts new file mode 100644 index 00000000000..eadf3bca810 --- /dev/null +++ b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts @@ -0,0 +1,62 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +import { createRule } from './createRule'; + +/** + * True for a numeric literal, including a negated one like `-1`. + */ +const isNumericLiteral = (node: TSESTree.Node | null | undefined): boolean => { + if (!node) return false; + if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number') { + return true; + } + return ( + node.type === AST_NODE_TYPES.UnaryExpression && + (node.operator === '-' || node.operator === '+') && + isNumericLiteral(node.argument) + ); +}; + +const isZIndexKey = (key: TSESTree.Node): boolean => + (key.type === AST_NODE_TYPES.Identifier && key.name === 'zIndex') || + (key.type === AST_NODE_TYPES.Literal && key.value === 'zIndex') || + (key.type === AST_NODE_TYPES.Literal && key.value === 'z-index'); + +export default createRule({ + create(context) { + return { + // Style objects: `{ zIndex: 1 }` / `{ 'z-index': 1 }` + Property(node) { + if (isZIndexKey(node.key) && isNumericLiteral(node.value)) { + context.report({ messageId: 'noRawZIndex', node: node.value }); + } + }, + // JSX props: `` + JSXAttribute(node) { + if ( + node.name.type === AST_NODE_TYPES.JSXIdentifier && + node.name.name === 'zIndex' && + node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && + isNumericLiteral(node.value.expression as TSESTree.Node) + ) { + context.report({ messageId: 'noRawZIndex', node: node.value }); + } + }, + }; + }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow raw numeric z-index values; use a semantic token from the `zIndices` scale.', + recommended: 'error', + }, + messages: { + noRawZIndex: + 'Use a semantic token from the `zIndices` scale (e.g. `zIndices.modal`) instead of a raw z-index number. For a deliberate in-between value, disable this rule inline with a justifying comment.', + }, + type: 'suggestion', + schema: [], + }, + name: 'no-raw-z-index', +}); diff --git a/packages/eslint-plugin-gamut/src/recommended.ts b/packages/eslint-plugin-gamut/src/recommended.ts index 970bb7700ff..a1d71b635c9 100644 --- a/packages/eslint-plugin-gamut/src/recommended.ts +++ b/packages/eslint-plugin-gamut/src/recommended.ts @@ -2,6 +2,7 @@ export default { rules: { 'gamut/no-css-standalone': 'error', 'gamut/no-inline-style': 'error', + 'gamut/no-raw-z-index': 'error', 'gamut/prefer-themed': 'off', 'gamut/gamut-import-paths': 'error', }, diff --git a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap index 2be2c419212..061f863b25a 100644 --- a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap +++ b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`themes admin - theme shape 1`] = ` { @@ -79,7 +79,7 @@ exports[`themes admin - theme shape 1`] = ` "base": "4rem", "md": "5rem", }, - "headerZ": 15, + "headerZ": 400, }, "modes": { "dark": { @@ -151,6 +151,20 @@ exports[`themes admin - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, + "zIndices": { + "appBar": 400, + "base": 0, + "flyout": 500, + "foreground": 1, + "modal": 600, + "popover": 700, + "portal": 200, + "stickyHeader": 100, + "toaster": 800, + "tooltip": 900, + "underlay": -1, + "widget": 300, + }, }, "_variables": { "mode": { @@ -257,7 +271,19 @@ exports[`themes admin - theme shape 1`] = ` "--color-yellow-500": "#FFD300", "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", - "--elements-headerZ": 15, + "--elements-headerZ": 400, + "--zIndices-appBar": 400, + "--zIndices-base": 0, + "--zIndices-flyout": 500, + "--zIndices-foreground": 1, + "--zIndices-modal": 600, + "--zIndices-popover": 700, + "--zIndices-portal": 200, + "--zIndices-stickyHeader": 100, + "--zIndices-toaster": 800, + "--zIndices-tooltip": 900, + "--zIndices-underlay": -1, + "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -513,6 +539,20 @@ monospace", "8": "0.5rem", "96": "6rem", }, + "zIndices": { + "appBar": "var(--zIndices-appBar)", + "base": "var(--zIndices-base)", + "flyout": "var(--zIndices-flyout)", + "foreground": "var(--zIndices-foreground)", + "modal": "var(--zIndices-modal)", + "popover": "var(--zIndices-popover)", + "portal": "var(--zIndices-portal)", + "stickyHeader": "var(--zIndices-stickyHeader)", + "toaster": "var(--zIndices-toaster)", + "tooltip": "var(--zIndices-tooltip)", + "underlay": "var(--zIndices-underlay)", + "widget": "var(--zIndices-widget)", + }, } `; @@ -595,7 +635,7 @@ exports[`themes core - theme shape 1`] = ` "base": "4rem", "md": "5rem", }, - "headerZ": 15, + "headerZ": 400, }, "modes": { "dark": { @@ -667,6 +707,20 @@ exports[`themes core - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, + "zIndices": { + "appBar": 400, + "base": 0, + "flyout": 500, + "foreground": 1, + "modal": 600, + "popover": 700, + "portal": 200, + "stickyHeader": 100, + "toaster": 800, + "tooltip": 900, + "underlay": -1, + "widget": 300, + }, }, "_variables": { "mode": { @@ -773,7 +827,19 @@ exports[`themes core - theme shape 1`] = ` "--color-yellow-500": "#FFD300", "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", - "--elements-headerZ": 15, + "--elements-headerZ": 400, + "--zIndices-appBar": 400, + "--zIndices-base": 0, + "--zIndices-flyout": 500, + "--zIndices-foreground": 1, + "--zIndices-modal": 600, + "--zIndices-popover": 700, + "--zIndices-portal": 200, + "--zIndices-stickyHeader": 100, + "--zIndices-toaster": 800, + "--zIndices-tooltip": 900, + "--zIndices-underlay": -1, + "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -1029,6 +1095,20 @@ monospace", "8": "0.5rem", "96": "6rem", }, + "zIndices": { + "appBar": "var(--zIndices-appBar)", + "base": "var(--zIndices-base)", + "flyout": "var(--zIndices-flyout)", + "foreground": "var(--zIndices-foreground)", + "modal": "var(--zIndices-modal)", + "popover": "var(--zIndices-popover)", + "portal": "var(--zIndices-portal)", + "stickyHeader": "var(--zIndices-stickyHeader)", + "toaster": "var(--zIndices-toaster)", + "tooltip": "var(--zIndices-tooltip)", + "underlay": "var(--zIndices-underlay)", + "widget": "var(--zIndices-widget)", + }, } `; @@ -1114,7 +1194,7 @@ exports[`themes lxStudio - theme shape 1`] = ` "base": "4rem", "md": "5rem", }, - "headerZ": 15, + "headerZ": 400, }, "modes": { "dark": { @@ -1186,6 +1266,20 @@ exports[`themes lxStudio - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, + "zIndices": { + "appBar": 400, + "base": 0, + "flyout": 500, + "foreground": 1, + "modal": 600, + "popover": 700, + "portal": 200, + "stickyHeader": 100, + "toaster": 800, + "tooltip": 900, + "underlay": -1, + "widget": 300, + }, }, "_variables": { "mode": { @@ -1295,7 +1389,19 @@ exports[`themes lxStudio - theme shape 1`] = ` "--color-yellow-500": "#FFD300", "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", - "--elements-headerZ": 15, + "--elements-headerZ": 400, + "--zIndices-appBar": 400, + "--zIndices-base": 0, + "--zIndices-flyout": 500, + "--zIndices-foreground": 1, + "--zIndices-modal": 600, + "--zIndices-popover": 700, + "--zIndices-portal": 200, + "--zIndices-stickyHeader": 100, + "--zIndices-toaster": 800, + "--zIndices-tooltip": 900, + "--zIndices-underlay": -1, + "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -1556,6 +1662,20 @@ monospace", "8": "0.5rem", "96": "6rem", }, + "zIndices": { + "appBar": "var(--zIndices-appBar)", + "base": "var(--zIndices-base)", + "flyout": "var(--zIndices-flyout)", + "foreground": "var(--zIndices-foreground)", + "modal": "var(--zIndices-modal)", + "popover": "var(--zIndices-popover)", + "portal": "var(--zIndices-portal)", + "stickyHeader": "var(--zIndices-stickyHeader)", + "toaster": "var(--zIndices-toaster)", + "tooltip": "var(--zIndices-tooltip)", + "underlay": "var(--zIndices-underlay)", + "widget": "var(--zIndices-widget)", + }, } `; @@ -1654,7 +1774,7 @@ exports[`themes percipio - theme shape 1`] = ` "base": "4rem", "md": "5rem", }, - "headerZ": 15, + "headerZ": 400, }, "modes": { "dark": { @@ -1726,6 +1846,20 @@ exports[`themes percipio - theme shape 1`] = ` "text-secondary": "rgba(34, 35, 37, 0.75)", }, }, + "zIndices": { + "appBar": 400, + "base": 0, + "flyout": 500, + "foreground": 1, + "modal": 600, + "popover": 700, + "portal": 200, + "stickyHeader": 100, + "toaster": 800, + "tooltip": 900, + "underlay": -1, + "widget": 300, + }, }, "_variables": { "mode": { @@ -1848,7 +1982,19 @@ exports[`themes percipio - theme shape 1`] = ` "--color-yellow-500": "#FFD300", "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", - "--elements-headerZ": 15, + "--elements-headerZ": 400, + "--zIndices-appBar": 400, + "--zIndices-base": 0, + "--zIndices-flyout": 500, + "--zIndices-foreground": 1, + "--zIndices-modal": 600, + "--zIndices-popover": 700, + "--zIndices-portal": 200, + "--zIndices-stickyHeader": 100, + "--zIndices-toaster": 800, + "--zIndices-tooltip": 900, + "--zIndices-underlay": -1, + "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -2120,6 +2266,20 @@ sans-serif", "8": "0.5rem", "96": "6rem", }, + "zIndices": { + "appBar": "var(--zIndices-appBar)", + "base": "var(--zIndices-base)", + "flyout": "var(--zIndices-flyout)", + "foreground": "var(--zIndices-foreground)", + "modal": "var(--zIndices-modal)", + "popover": "var(--zIndices-popover)", + "portal": "var(--zIndices-portal)", + "stickyHeader": "var(--zIndices-stickyHeader)", + "toaster": "var(--zIndices-toaster)", + "tooltip": "var(--zIndices-tooltip)", + "underlay": "var(--zIndices-underlay)", + "widget": "var(--zIndices-widget)", + }, } `; @@ -2222,7 +2382,7 @@ exports[`themes platform - theme shape 1`] = ` "base": "4rem", "md": "5rem", }, - "headerZ": 15, + "headerZ": 400, }, "modes": { "dark": { @@ -2346,6 +2506,20 @@ exports[`themes platform - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, + "zIndices": { + "appBar": 400, + "base": 0, + "flyout": 500, + "foreground": 1, + "modal": 600, + "popover": 700, + "portal": 200, + "stickyHeader": 100, + "toaster": 800, + "tooltip": 900, + "underlay": -1, + "widget": 300, + }, }, "_variables": { "mode": { @@ -2498,7 +2672,19 @@ exports[`themes platform - theme shape 1`] = ` "--color-yellow-500": "#FFD300", "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", - "--elements-headerZ": 15, + "--elements-headerZ": 400, + "--zIndices-appBar": 400, + "--zIndices-base": 0, + "--zIndices-flyout": 500, + "--zIndices-foreground": 1, + "--zIndices-modal": 600, + "--zIndices-popover": 700, + "--zIndices-portal": 200, + "--zIndices-stickyHeader": 100, + "--zIndices-toaster": 800, + "--zIndices-tooltip": 900, + "--zIndices-underlay": -1, + "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -2852,5 +3038,19 @@ monospace", "8": "0.5rem", "96": "6rem", }, + "zIndices": { + "appBar": "var(--zIndices-appBar)", + "base": "var(--zIndices-base)", + "flyout": "var(--zIndices-flyout)", + "foreground": "var(--zIndices-foreground)", + "modal": "var(--zIndices-modal)", + "popover": "var(--zIndices-popover)", + "portal": "var(--zIndices-portal)", + "stickyHeader": "var(--zIndices-stickyHeader)", + "toaster": "var(--zIndices-toaster)", + "tooltip": "var(--zIndices-tooltip)", + "underlay": "var(--zIndices-underlay)", + "widget": "var(--zIndices-widget)", + }, } `; diff --git a/packages/gamut-styles/src/themes/core.ts b/packages/gamut-styles/src/themes/core.ts index 3a6da844330..eb4457cdbea 100644 --- a/packages/gamut-styles/src/themes/core.ts +++ b/packages/gamut-styles/src/themes/core.ts @@ -11,6 +11,7 @@ import { lineHeight, mediaQueries, spacing, + zIndices, } from '../variables'; /** @@ -28,6 +29,7 @@ export const coreTheme = createTheme({ fontWeight, spacing, elements, + zIndices, }) .addColors(corePalette) .addColorModes('light', { @@ -141,6 +143,7 @@ export const coreTheme = createTheme({ 2: `2px solid ${colors['border-primary']}`, })) .createScaleVariables('elements') + .createScaleVariables('zIndices') .addName('core') .build(); diff --git a/packages/gamut-styles/src/variables/elements.ts b/packages/gamut-styles/src/variables/elements.ts index 25bc743bd83..387199e0f28 100644 --- a/packages/gamut-styles/src/variables/elements.ts +++ b/packages/gamut-styles/src/variables/elements.ts @@ -1,9 +1,12 @@ +import { zIndices } from './zIndices'; + export const elements = { headerHeight: { base: '4rem', md: '5rem' }, /** - * Semi-arbitrary z-index for global page headers. - * @remarks PLEASE talk to web platform before adding new z-index constants! + * z-index for global page headers. Aliases the `appBar` token from the `zIndices` + * scale so consumers still reading `elements.headerZ` stay in sync with the scale. + * Prefer `zIndex="appBar"` in new code. */ - headerZ: 15, + headerZ: zIndices.appBar, } as const; diff --git a/packages/gamut-styles/src/variables/index.ts b/packages/gamut-styles/src/variables/index.ts index d4f074462ef..06ad877133d 100644 --- a/packages/gamut-styles/src/variables/index.ts +++ b/packages/gamut-styles/src/variables/index.ts @@ -5,3 +5,4 @@ export * from './responsive'; export * from './spacing'; export * from './timing'; export * from './typography'; +export * from './zIndices'; diff --git a/packages/gamut-styles/src/variables/zIndices.ts b/packages/gamut-styles/src/variables/zIndices.ts new file mode 100644 index 00000000000..24fcadd51d6 --- /dev/null +++ b/packages/gamut-styles/src/variables/zIndices.ts @@ -0,0 +1,43 @@ +/** + * Semantic z-index scale. A single scale covers both in-flow layers (low values) and + * portal layers (≥100). Every z-index in Gamut should reference a token here rather than a + * magic number. + * + * The `zIndex` prop is intentionally left numeric/unscaled, and this object is numeric, so + * tokens are used as `zIndex={zIndices.modal}`. That preserves the escape hatch (a raw + * in-between number, e.g. `zIndex={605}`) and arithmetic on tokens (e.g. `zIndices.foreground - 2`). + * + * Portal-band values are spaced by 100 so in-between escape-hatch numbers are available. + * `portal` (200) is the floor of the portal band and the default for `BodyPortal`. + * + * @remarks PLEASE talk to web platform before adding new z-index tokens. + */ +export const zIndices = { + /** Decorative layer behind content (underlines, backdrops, shadows). */ + underlay: -1, + /** Ground layer — establishes a local stacking context without lifting above siblings. */ + base: 0, + /** Content lifted just above a sibling underlay/base. */ + foreground: 1, + /** + * In-flow sticky content headers (e.g. a sticky table `thead`). Below the portal band, + * so every overlay and the app nav cover it. Not a portal layer. + */ + stickyHeader: 100, + /** Portal floor / `BodyPortal` default — un-tokenized portal content lands here. */ + portal: 200, + /** Persistent floating page furniture at rest (e.g. an AI chat launcher, help bubble). */ + widget: 300, + /** Global app header / nav bar. Aliased by the legacy `elements.headerZ` constant. */ + appBar: 400, + /** Portaled side panel (the `Flyout` component = `Drawer` inside `Overlay`). */ + flyout: 500, + /** `Overlay`, `Modal`, and `Dialog` (they share one portal primitive). */ + modal: 600, + /** Portal-mode `Popover` and the portaled `SelectDropdown` menu — above modal. */ + popover: 700, + /** Toasts / notifications — visible above modals. */ + toaster: 800, + /** Floating tooltips (`FloatingTip`) — highest, so they are never clipped. */ + tooltip: 900, +} as const; diff --git a/packages/gamut-styles/src/variance/config.ts b/packages/gamut-styles/src/variance/config.ts index ad2b6e3fc23..1cacdaef3f1 100644 --- a/packages/gamut-styles/src/variance/config.ts +++ b/packages/gamut-styles/src/variance/config.ts @@ -328,6 +328,9 @@ export const positioning = { resolveProperty: getPropertyMode, transform: transformSize, }, + // Intentionally unscaled: scaled variance props are token-only and reject raw numbers, + // which would break the numeric escape hatch and Tip's z-index arithmetic. Use the numeric + // `zIndices` token object instead (e.g. zIndex={zIndices.modal}). zIndex: { property: 'zIndex' }, opacity: { property: 'opacity' }, } as const; diff --git a/packages/gamut/src/Anchor/index.tsx b/packages/gamut/src/Anchor/index.tsx index 287ff0e6d37..b65906656eb 100644 --- a/packages/gamut/src/Anchor/index.tsx +++ b/packages/gamut/src/Anchor/index.tsx @@ -1,4 +1,9 @@ -import { styledOptions, system, variant } from '@codecademy/gamut-styles'; +import { + styledOptions, + system, + variant, + zIndices, +} from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; import { ComponentProps, forwardRef, HTMLProps, RefObject } from 'react'; @@ -21,7 +26,7 @@ const outlineFocusVisible = { border: 2, borderColor: 'primary', opacity: 0, - zIndex: 0, + zIndex: zIndices.base, }, [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { diff --git a/packages/gamut/src/BarChart/layout/GridLines.tsx b/packages/gamut/src/BarChart/layout/GridLines.tsx index 14db738f132..bf1c9718c98 100644 --- a/packages/gamut/src/BarChart/layout/GridLines.tsx +++ b/packages/gamut/src/BarChart/layout/GridLines.tsx @@ -1,4 +1,4 @@ -import { css } from '@codecademy/gamut-styles'; +import { css, zIndices } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import React, { useMemo } from 'react'; @@ -12,7 +12,7 @@ const GridLineWrapper = styled(Box)( inset: 0, pointerEvents: 'none', position: 'absolute', - zIndex: 0, + zIndex: zIndices.base, }) ); diff --git a/packages/gamut/src/BodyPortal/index.tsx b/packages/gamut/src/BodyPortal/index.tsx index 5da951b486b..34309f27739 100644 --- a/packages/gamut/src/BodyPortal/index.tsx +++ b/packages/gamut/src/BodyPortal/index.tsx @@ -1,4 +1,9 @@ -import { ColorMode, system, useCurrentMode } from '@codecademy/gamut-styles'; +import { + ColorMode, + system, + useCurrentMode, + zIndices, +} from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useState } from 'react'; import * as React from 'react'; @@ -19,16 +24,17 @@ const PortalWrapper = styled interface BodyPortalProps { /** - * TEMPORARY: a stopgap solution to avoid zIndex conflicts - - * will be reworked with: GM-624 - * previously, zIndex was set to 1 in the CSS function + * Stacking layer for the portaled content. Pass a `zIndices` token + * (e.g. `zIndices.modal`) or a raw number as an escape hatch. Defaults to + * `zIndices.portal` — the floor of the portal band — so un-tokenized portal content + * stays above local page content instead of silently landing at a low value. */ zIndex?: number; } export const BodyPortal: React.FC> = ({ children, - zIndex = 1, + zIndex = zIndices.portal, }) => { const [ready, setReady] = useState(false); const mode = useCurrentMode(); diff --git a/packages/gamut/src/Box/props.ts b/packages/gamut/src/Box/props.ts index 536ff575392..0d53c0a39a9 100644 --- a/packages/gamut/src/Box/props.ts +++ b/packages/gamut/src/Box/props.ts @@ -1,4 +1,4 @@ -import { system } from '@codecademy/gamut-styles'; +import { system, zIndices } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import { WithChildrenProp } from '../utils'; @@ -24,7 +24,7 @@ export const sharedStates = system.states({ }, context: { position: 'relative', - zIndex: 1, + zIndex: zIndices.foreground, }, 'no-select': { WebkitTouchCallout: 'none', diff --git a/packages/gamut/src/Button/shared/styles.ts b/packages/gamut/src/Button/shared/styles.ts index 9bcf0cbed8f..534aa43b7de 100644 --- a/packages/gamut/src/Button/shared/styles.ts +++ b/packages/gamut/src/Button/shared/styles.ts @@ -4,6 +4,7 @@ import { styledOptions, system, transitionConcat, + zIndices, } from '@codecademy/gamut-styles'; import { CSSObject, ThemeProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -67,7 +68,7 @@ export const buttonStyles = system.css({ border: 2, inset: -5, opacity: 0, - zIndex: 0, + zIndex: zIndices.base, }, [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { opacity: 1, diff --git a/packages/gamut/src/DataList/EmptyRows.tsx b/packages/gamut/src/DataList/EmptyRows.tsx index 3684664b57e..811fb5fb99a 100644 --- a/packages/gamut/src/DataList/EmptyRows.tsx +++ b/packages/gamut/src/DataList/EmptyRows.tsx @@ -1,3 +1,5 @@ +import { zIndices } from '@codecademy/gamut-styles'; + import { FlexBox } from '../Box'; import { FillButton } from '../Button'; import { Text } from '../Typography'; @@ -15,7 +17,7 @@ export const EmptyRows = () => { position="sticky" top="calc(50% - 66px)" width="320px" - zIndex={1} + zIndex={zIndices.foreground} > diff --git a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx index 425348fa6dd..29e2b477461 100644 --- a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx +++ b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx @@ -1,4 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; +import { zIndices } from '@codecademy/gamut-styles'; import * as React from 'react'; import { Box } from '../../../Box'; @@ -12,7 +13,7 @@ export const CalendarWrapper: React.FC = ({ children }) => ( border={1} borderRadius="sm" position="relative" - zIndex={1} + zIndex={zIndices.foreground} > {children} diff --git a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx index b5a7354e8d7..6a992bb7969 100644 --- a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx +++ b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx @@ -1,4 +1,9 @@ -import { css, states, transitionConcat } from '@codecademy/gamut-styles'; +import { + css, + states, + transitionConcat, + zIndices, +} from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -119,7 +124,7 @@ export const DateCell = styled.td( borderRadius: 'lg', border: 2, opacity: 0, - zIndex: 0, + zIndex: zIndices.base, }, '&:focus-visible::before': { opacity: 1, diff --git a/packages/gamut/src/Flyout/index.tsx b/packages/gamut/src/Flyout/index.tsx index 8bd38fc0ccf..adfee0e0c4c 100644 --- a/packages/gamut/src/Flyout/index.tsx +++ b/packages/gamut/src/Flyout/index.tsx @@ -1,5 +1,5 @@ import { MiniDeleteIcon } from '@codecademy/gamut-icons'; -import { Background, Colors } from '@codecademy/gamut-styles'; +import { Background, Colors, zIndices } from '@codecademy/gamut-styles'; import * as React from 'react'; import { FlexBox } from '../Box'; @@ -52,6 +52,7 @@ export const Flyout: React.FC = ({ escapeCloses isOpen={expanded} shroud + zIndex={zIndices.flyout} onRequestClose={onClose} > diff --git a/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx b/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx index f31839eaa46..603b626cbfe 100644 --- a/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx +++ b/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx @@ -276,6 +276,9 @@ export const SelectDropdown: React.FC = ({ isOptionDisabled={(option) => option.disabled} isSearchable={isSearchable} menuAlignment={menuAlignment} + menuPortalTarget={ + typeof document !== 'undefined' ? document.body : undefined + } name={name} options={selectOptions} placeholder={placeholder} diff --git a/packages/gamut/src/Form/SelectDropdown/styles.ts b/packages/gamut/src/Form/SelectDropdown/styles.ts index ba9178c113c..281e1e87f8c 100644 --- a/packages/gamut/src/Form/SelectDropdown/styles.ts +++ b/packages/gamut/src/Form/SelectDropdown/styles.ts @@ -3,6 +3,7 @@ import { states, theme as GamutTheme, variant, + zIndices, } from '@codecademy/gamut-styles'; import { StylesConfig } from 'react-select'; @@ -25,7 +26,6 @@ import { BaseSelectComponentProps } from './types/styles'; const selectDropdownStyles = css({ ...formBaseFieldStylesObject, display: 'flex', - zIndex: 3, }); const selectFocusStyles = { @@ -164,6 +164,12 @@ export const getMemoizedStyles = ( : {}), }; }, + menuPortal: (provided) => ({ + ...provided, + // The menu is portaled to the body, so it stacks at the page root as a popover — + // above sticky headers and modal content. A raw `zIndex` prop overrides as an escape hatch. + zIndex: zIndex ?? zIndices.popover, + }), menuList: (provided, state: BaseSelectComponentProps) => { const sizeInteger = state.selectProps.size === 'small' ? 2 : 3; const maxHeight = `${ diff --git a/packages/gamut/src/List/TableHeader.tsx b/packages/gamut/src/List/TableHeader.tsx index 60810da9c91..e7c64cfe86b 100644 --- a/packages/gamut/src/List/TableHeader.tsx +++ b/packages/gamut/src/List/TableHeader.tsx @@ -1,3 +1,4 @@ +import { zIndices } from '@codecademy/gamut-styles'; import { ComponentProps, forwardRef } from 'react'; import { Box } from '../Box'; @@ -12,7 +13,13 @@ export const TableHeader = forwardRef( ({ children, ...rest }, ref) => { const { spacing, scrollable, variant } = useListContext(); return ( - + ( flexDirection: { _: 'row', c_base: 'column', c_sm: 'row' }, top: 0, bg: 'background-current', - zIndex: 2, + zIndex: zIndices.stickyHeader, fontFamily: 'accent', pb: { _: 0, c_base: 8, c_sm: 0 }, }), @@ -468,7 +469,7 @@ export const StickyHeaderColWrapper = styled.th( height: '100%', top: 0, left: 0, - zIndex: -1, + zIndex: zIndices.underlay, }, '&:after': { content: '""', @@ -482,7 +483,7 @@ export const StickyHeaderColWrapper = styled.th( height: '100%', top: 0, left: 0, - zIndex: -1, + zIndex: zIndices.underlay, }, // p: 0 removes the browser's default padding of 1px p: 0, @@ -490,7 +491,7 @@ export const StickyHeaderColWrapper = styled.th( flexShrink: 0, position: 'sticky', left: 0, - zIndex: 1, + zIndex: zIndices.foreground, bg: { _: 'inherit', c_base: 'transparent', c_sm: 'inherit' }, '&:not(:first-of-type)': { left: { _: 16, c_base: 0, c_sm: 16 }, diff --git a/packages/gamut/src/Menu/elements.tsx b/packages/gamut/src/Menu/elements.tsx index f8eea4af20d..0a36aed2f91 100644 --- a/packages/gamut/src/Menu/elements.tsx +++ b/packages/gamut/src/Menu/elements.tsx @@ -2,6 +2,7 @@ import { styledOptions, system, transitionConcat, + zIndices, } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -88,7 +89,7 @@ const interactiveVariants = system.variant({ alignItems: 'center', cursor: 'pointer', width: 1, - zIndex: 1, + zIndex: zIndices.foreground, px: 24, py: 12, position: 'relative', @@ -108,7 +109,7 @@ const interactiveVariants = system.variant({ border: 2, borderColor: 'primary', opacity: 0, - zIndex: -1, + zIndex: zIndices.underlay, }, [MenuItemSelectors.OUTLINE_FOCUS_VISIBLE]: { opacity: 1, @@ -224,7 +225,7 @@ const StyledListLink = styled('a', styledOptions<'a'>())( export const ListLink = forwardRef< HTMLAnchorElement, ComponentProps ->(({ zIndex = 1, ...rest }, ref) => ( +>(({ zIndex = zIndices.foreground, ...rest }, ref) => ( )); diff --git a/packages/gamut/src/Overlay/index.tsx b/packages/gamut/src/Overlay/index.tsx index 9ddda4854d0..e4512961341 100644 --- a/packages/gamut/src/Overlay/index.tsx +++ b/packages/gamut/src/Overlay/index.tsx @@ -1,4 +1,4 @@ -import { states } from '@codecademy/gamut-styles'; +import { states, zIndices } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useCallback } from 'react'; import * as React from 'react'; @@ -34,8 +34,9 @@ export type OverlayProps = { /** Whether the overlay allows scroll */ allowScroll?: boolean; /** - * z-index for the Overlay. Defaults to 3 to appear above common UI elements - * like headers . Can be overridden when needed for custom stacking orders. + * Stacking layer for the Overlay. Pass a `zIndices` token or a raw number (escape + * hatch). Defaults to `zIndices.modal`; a portaled side panel should pass + * `zIndices.flyout` to sit below modals. */ zIndex?: number; }; @@ -61,7 +62,7 @@ export const Overlay: React.FC = ({ onRequestClose, isOpen, allowScroll = false, - zIndex = 3, + zIndex = zIndices.modal, }) => { const handleOutsideClick = useCallback(() => { if (clickOutsideCloses) { diff --git a/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx b/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx index c3d6bcd8b15..3bf50b9e0be 100644 --- a/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx +++ b/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx @@ -1,5 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; -import { styledOptions, system } from '@codecademy/gamut-styles'; +import { styledOptions, system, zIndices } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; import { ComponentProps, forwardRef } from 'react'; @@ -13,7 +13,7 @@ const PatternBackdropBody = styled('div', styledOptions)< >( system.css({ position: 'relative', - zIndex: 1, + zIndex: zIndices.foreground, bg: 'background', border: 1, maxWidth: 1, @@ -29,7 +29,12 @@ type PatternBackdropProps = ComponentProps; */ export const PatternBackdrop = forwardRef( ({ children, ...rest }, ref) => ( - + {isOpen && ( - + ) : ( - + ); export type PopoverContainerProps = Pick; diff --git a/packages/gamut/src/Popover/styles/base.ts b/packages/gamut/src/Popover/styles/base.ts index d3aae9f9c2c..59fcf3c5545 100644 --- a/packages/gamut/src/Popover/styles/base.ts +++ b/packages/gamut/src/Popover/styles/base.ts @@ -1,4 +1,4 @@ -import { states, variant } from '@codecademy/gamut-styles'; +import { states, variant, zIndices } from '@codecademy/gamut-styles'; import { toolTipBodyCss } from '../../Tip/shared/styles/styles'; @@ -31,7 +31,7 @@ export const popoverStates = states({ export const raisedDivVariants = variant({ base: { - zIndex: 1, + zIndex: zIndices.foreground, }, defaultVariant: 'primary', variants: { diff --git a/packages/gamut/src/Popover/styles/variants.ts b/packages/gamut/src/Popover/styles/variants.ts index 1ebf78d59e7..ba22712decf 100644 --- a/packages/gamut/src/Popover/styles/variants.ts +++ b/packages/gamut/src/Popover/styles/variants.ts @@ -1,4 +1,4 @@ -import { states, variant } from '@codecademy/gamut-styles'; +import { states, variant, zIndices } from '@codecademy/gamut-styles'; import { createVariantsFromAlignments } from '../../Tip/shared/styles/createVariantsUtils'; import { tooltipArrowHeight } from '../../Tip/shared/styles/styles'; @@ -47,7 +47,7 @@ const beakVariantStyles = createVariantsFromAlignments( export const beakVariants = variant({ base: { background: 'transparent', - zIndex: 1, + zIndex: zIndices.foreground, position: 'fixed', }, prop: 'beak', diff --git a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx index c93f1965bc8..0259a1e9272 100644 --- a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx +++ b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx @@ -1,4 +1,9 @@ -import { elementDir, system, useElementDir } from '@codecademy/gamut-styles'; +import { + elementDir, + system, + useElementDir, + zIndices, +} from '@codecademy/gamut-styles'; import { variance } from '@codecademy/variance'; import styled from '@emotion/styled'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -282,5 +287,5 @@ export const PopoverContainer: React.FC = ({ if (inline) return content; - return {content}; + return {content}; }; diff --git a/packages/gamut/src/Tabs/TabButton.tsx b/packages/gamut/src/Tabs/TabButton.tsx index 0cbaf3f2582..fc150c6560a 100644 --- a/packages/gamut/src/Tabs/TabButton.tsx +++ b/packages/gamut/src/Tabs/TabButton.tsx @@ -1,4 +1,4 @@ -import { states, variant } from '@codecademy/gamut-styles'; +import { states, variant, zIndices } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -29,7 +29,7 @@ const focusVisibleStyles = { borderColor: 'primary', position: 'absolute', inset: 0, - zIndex: 0, + zIndex: zIndices.base, borderRadiusTop: 'md', borderRadiusBottom: 'none', }, @@ -48,7 +48,7 @@ export const tabVariants = variant({ color: 'text', font: 'inherit', cursor: 'pointer', - zIndex: 1, + zIndex: zIndices.foreground, [TabSelectors.DISABLED]: { opacity: 0.25, cursor: 'not-allowed', diff --git a/packages/gamut/src/Tabs/Tabs.tsx b/packages/gamut/src/Tabs/Tabs.tsx index 3194df06756..13332160986 100644 --- a/packages/gamut/src/Tabs/Tabs.tsx +++ b/packages/gamut/src/Tabs/Tabs.tsx @@ -1,4 +1,4 @@ -import { Background } from '@codecademy/gamut-styles'; +import { Background, zIndices } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; import * as React from 'react'; @@ -25,10 +25,10 @@ export const Tabs: React.FC = (props) => { {/* currently only supporting dark mode for the LE variant. */} {props.variant === 'block' ? ( - + ) : ( - + )} ); diff --git a/packages/gamut/src/Tabs/styles.tsx b/packages/gamut/src/Tabs/styles.tsx index 68d2579f9fb..e445e6c8686 100644 --- a/packages/gamut/src/Tabs/styles.tsx +++ b/packages/gamut/src/Tabs/styles.tsx @@ -1,4 +1,4 @@ -import { system, variant } from '@codecademy/gamut-styles'; +import { system, variant, zIndices } from '@codecademy/gamut-styles'; export const tabContainerVariants = variant({ base: { @@ -15,7 +15,7 @@ export const tabContainerVariants = variant({ bg: 'text', position: 'absolute', bottom: 0, - zIndex: 0, + zIndex: zIndices.base, width: '100%', }, }, diff --git a/packages/gamut/src/Tip/PreviewTip/elements.tsx b/packages/gamut/src/Tip/PreviewTip/elements.tsx index 6f42308c3c4..21cde41ef91 100644 --- a/packages/gamut/src/Tip/PreviewTip/elements.tsx +++ b/packages/gamut/src/Tip/PreviewTip/elements.tsx @@ -1,5 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; -import { css, variant } from '@codecademy/gamut-styles'; +import { css, variant, zIndices } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useMemo } from 'react'; @@ -147,7 +147,7 @@ export const PreviewTipShadow: React.FC = ({ return ( diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index 871cfaac1ac..b59608e1459 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -1,3 +1,5 @@ +import { zIndices } from '@codecademy/gamut-styles'; + import { InfoTipContainer } from '../InfoTip/styles'; import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements'; import { ToolTipContainer } from '../ToolTip/elements'; @@ -55,7 +57,7 @@ export const InlineTip: React.FC = ({ const tipBody = ( = ({ colorMode = 'light', }) => { return ( - + diff --git a/packages/gamut/src/Typography/Text.tsx b/packages/gamut/src/Typography/Text.tsx index 173661b8632..a05bf9d53d0 100644 --- a/packages/gamut/src/Typography/Text.tsx +++ b/packages/gamut/src/Typography/Text.tsx @@ -3,6 +3,7 @@ import { styledOptions, system, variant, + zIndices, } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -76,7 +77,7 @@ const textStates = states({ fontWeight: 'bold', minWidth: '0.4rem', position: 'relative', - zIndex: 1, + zIndex: zIndices.foreground, // the text is more legible against the background color with text smoothing MozOsxFontSmoothing: 'grayscale', WebkitFontSmoothing: 'antialiased', @@ -90,7 +91,7 @@ const textStates = states({ position: 'absolute', top: '50%', width: 'calc(100% + 0.4rem)', - zIndex: -1, + zIndex: zIndices.underlay, }, }, screenreader: { From c01bd808cbfa4625c569c8f3c300f9161cc6175c Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 12:21:06 -0400 Subject: [PATCH 02/10] update eslint messages and desc --- packages/eslint-plugin-gamut/src/no-raw-z-index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-gamut/src/no-raw-z-index.ts b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts index eadf3bca810..3a0ca1e55eb 100644 --- a/packages/eslint-plugin-gamut/src/no-raw-z-index.ts +++ b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts @@ -48,12 +48,12 @@ export default createRule({ meta: { docs: { description: - 'Disallow raw numeric z-index values; use a semantic token from the `zIndices` scale.', + 'Discourage raw numeric z-index values which can lead to z-index stacking issues and encourage usage of semantic tokens from the `zIndices` scale.', recommended: 'error', }, messages: { noRawZIndex: - 'Use a semantic token from the `zIndices` scale (e.g. `zIndices.modal`) instead of a raw z-index number. For a deliberate in-between value, disable this rule inline with a justifying comment.', + 'Semantic tokens from the `zIndices` scale (e.g. `zIndices.modal`) are recommendedinstead of a raw z-index number. For a deliberate in-between value, disable this rule inline with a justifying comment.', }, type: 'suggestion', schema: [], From 96635b09a194797d939325afddbfa2b605557a42 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 14:27:49 -0400 Subject: [PATCH 03/10] updates to zIndex scale --- .../__snapshots__/theme.test.ts.snap | 35 +++++----------- .../gamut-styles/src/variables/zIndices.ts | 12 +++--- packages/gamut/src/List/TableHeader.tsx | 2 +- packages/gamut/src/List/elements.tsx | 2 +- .../DataList/DataList.stories.tsx | 40 +++++++++---------- 5 files changed, 38 insertions(+), 53 deletions(-) diff --git a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap index 061f863b25a..2769c9de387 100644 --- a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap +++ b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap @@ -155,11 +155,10 @@ exports[`themes admin - theme shape 1`] = ` "appBar": 400, "base": 0, "flyout": 500, - "foreground": 1, + "foreground": 100, "modal": 600, "popover": 700, "portal": 200, - "stickyHeader": 100, "toaster": 800, "tooltip": 900, "underlay": -1, @@ -275,11 +274,10 @@ exports[`themes admin - theme shape 1`] = ` "--zIndices-appBar": 400, "--zIndices-base": 0, "--zIndices-flyout": 500, - "--zIndices-foreground": 1, + "--zIndices-foreground": 100, "--zIndices-modal": 600, "--zIndices-popover": 700, "--zIndices-portal": 200, - "--zIndices-stickyHeader": 100, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, "--zIndices-underlay": -1, @@ -547,7 +545,6 @@ monospace", "modal": "var(--zIndices-modal)", "popover": "var(--zIndices-popover)", "portal": "var(--zIndices-portal)", - "stickyHeader": "var(--zIndices-stickyHeader)", "toaster": "var(--zIndices-toaster)", "tooltip": "var(--zIndices-tooltip)", "underlay": "var(--zIndices-underlay)", @@ -711,11 +708,10 @@ exports[`themes core - theme shape 1`] = ` "appBar": 400, "base": 0, "flyout": 500, - "foreground": 1, + "foreground": 100, "modal": 600, "popover": 700, "portal": 200, - "stickyHeader": 100, "toaster": 800, "tooltip": 900, "underlay": -1, @@ -831,11 +827,10 @@ exports[`themes core - theme shape 1`] = ` "--zIndices-appBar": 400, "--zIndices-base": 0, "--zIndices-flyout": 500, - "--zIndices-foreground": 1, + "--zIndices-foreground": 100, "--zIndices-modal": 600, "--zIndices-popover": 700, "--zIndices-portal": 200, - "--zIndices-stickyHeader": 100, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, "--zIndices-underlay": -1, @@ -1103,7 +1098,6 @@ monospace", "modal": "var(--zIndices-modal)", "popover": "var(--zIndices-popover)", "portal": "var(--zIndices-portal)", - "stickyHeader": "var(--zIndices-stickyHeader)", "toaster": "var(--zIndices-toaster)", "tooltip": "var(--zIndices-tooltip)", "underlay": "var(--zIndices-underlay)", @@ -1270,11 +1264,10 @@ exports[`themes lxStudio - theme shape 1`] = ` "appBar": 400, "base": 0, "flyout": 500, - "foreground": 1, + "foreground": 100, "modal": 600, "popover": 700, "portal": 200, - "stickyHeader": 100, "toaster": 800, "tooltip": 900, "underlay": -1, @@ -1393,11 +1386,10 @@ exports[`themes lxStudio - theme shape 1`] = ` "--zIndices-appBar": 400, "--zIndices-base": 0, "--zIndices-flyout": 500, - "--zIndices-foreground": 1, + "--zIndices-foreground": 100, "--zIndices-modal": 600, "--zIndices-popover": 700, "--zIndices-portal": 200, - "--zIndices-stickyHeader": 100, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, "--zIndices-underlay": -1, @@ -1670,7 +1662,6 @@ monospace", "modal": "var(--zIndices-modal)", "popover": "var(--zIndices-popover)", "portal": "var(--zIndices-portal)", - "stickyHeader": "var(--zIndices-stickyHeader)", "toaster": "var(--zIndices-toaster)", "tooltip": "var(--zIndices-tooltip)", "underlay": "var(--zIndices-underlay)", @@ -1850,11 +1841,10 @@ exports[`themes percipio - theme shape 1`] = ` "appBar": 400, "base": 0, "flyout": 500, - "foreground": 1, + "foreground": 100, "modal": 600, "popover": 700, "portal": 200, - "stickyHeader": 100, "toaster": 800, "tooltip": 900, "underlay": -1, @@ -1986,11 +1976,10 @@ exports[`themes percipio - theme shape 1`] = ` "--zIndices-appBar": 400, "--zIndices-base": 0, "--zIndices-flyout": 500, - "--zIndices-foreground": 1, + "--zIndices-foreground": 100, "--zIndices-modal": 600, "--zIndices-popover": 700, "--zIndices-portal": 200, - "--zIndices-stickyHeader": 100, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, "--zIndices-underlay": -1, @@ -2274,7 +2263,6 @@ sans-serif", "modal": "var(--zIndices-modal)", "popover": "var(--zIndices-popover)", "portal": "var(--zIndices-portal)", - "stickyHeader": "var(--zIndices-stickyHeader)", "toaster": "var(--zIndices-toaster)", "tooltip": "var(--zIndices-tooltip)", "underlay": "var(--zIndices-underlay)", @@ -2510,11 +2498,10 @@ exports[`themes platform - theme shape 1`] = ` "appBar": 400, "base": 0, "flyout": 500, - "foreground": 1, + "foreground": 100, "modal": 600, "popover": 700, "portal": 200, - "stickyHeader": 100, "toaster": 800, "tooltip": 900, "underlay": -1, @@ -2676,11 +2663,10 @@ exports[`themes platform - theme shape 1`] = ` "--zIndices-appBar": 400, "--zIndices-base": 0, "--zIndices-flyout": 500, - "--zIndices-foreground": 1, + "--zIndices-foreground": 100, "--zIndices-modal": 600, "--zIndices-popover": 700, "--zIndices-portal": 200, - "--zIndices-stickyHeader": 100, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, "--zIndices-underlay": -1, @@ -3046,7 +3032,6 @@ monospace", "modal": "var(--zIndices-modal)", "popover": "var(--zIndices-popover)", "portal": "var(--zIndices-portal)", - "stickyHeader": "var(--zIndices-stickyHeader)", "toaster": "var(--zIndices-toaster)", "tooltip": "var(--zIndices-tooltip)", "underlay": "var(--zIndices-underlay)", diff --git a/packages/gamut-styles/src/variables/zIndices.ts b/packages/gamut-styles/src/variables/zIndices.ts index 24fcadd51d6..873d947a8ac 100644 --- a/packages/gamut-styles/src/variables/zIndices.ts +++ b/packages/gamut-styles/src/variables/zIndices.ts @@ -14,16 +14,16 @@ */ export const zIndices = { /** Decorative layer behind content (underlines, backdrops, shadows). */ - underlay: -1, + underlay: -100, /** Ground layer — establishes a local stacking context without lifting above siblings. */ base: 0, - /** Content lifted just above a sibling underlay/base. */ - foreground: 1, /** - * In-flow sticky content headers (e.g. a sticky table `thead`). Below the portal band, - * so every overlay and the app nav cover it. Not a portal layer. + * The raised in-flow layer: an element in front of what sits/scrolls behind it, but below + * all portal overlays. Covers content lifted above an `underlay` (e.g. text over its + * underline) and sticky content headers (e.g. a sticky table `thead`). Set to 100 so it + * clears common ad-hoc low z-index values. */ - stickyHeader: 100, + foreground: 100, /** Portal floor / `BodyPortal` default — un-tokenized portal content lands here. */ portal: 200, /** Persistent floating page furniture at rest (e.g. an AI chat launcher, help bubble). */ diff --git a/packages/gamut/src/List/TableHeader.tsx b/packages/gamut/src/List/TableHeader.tsx index e7c64cfe86b..63add861255 100644 --- a/packages/gamut/src/List/TableHeader.tsx +++ b/packages/gamut/src/List/TableHeader.tsx @@ -18,7 +18,7 @@ export const TableHeader = forwardRef( display="block" position="sticky" top={0} - zIndex={zIndices.stickyHeader} + zIndex={zIndices.foreground} > ( flexDirection: { _: 'row', c_base: 'column', c_sm: 'row' }, top: 0, bg: 'background-current', - zIndex: zIndices.stickyHeader, + zIndex: zIndices.foreground, fontFamily: 'accent', pb: { _: 0, c_base: 8, c_sm: 0 }, }), diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 27d0fbde385..f065ace0a80 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -101,26 +101,26 @@ const meta: Meta = { header: false, spacing: 'condensed', onRowSelect: () => {}, - onRowExpand: () => {}, - expandedContent: ({ row }) => ( - - - - ), + // onRowExpand: () => {}, + // expandedContent: ({ row }) => ( + // + // + // + // ), }, }; From 4d0c25cad40253ff00cc4e162070eee705329ebc Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 15:52:16 -0400 Subject: [PATCH 04/10] added a story --- .../styleguide/src/lib/Foundations/ZIndex.mdx | 75 ++++++++++++++++ .../src/lib/Foundations/shared/elements.tsx | 86 +++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 packages/styleguide/src/lib/Foundations/ZIndex.mdx diff --git a/packages/styleguide/src/lib/Foundations/ZIndex.mdx b/packages/styleguide/src/lib/Foundations/ZIndex.mdx new file mode 100644 index 00000000000..d597f7f639f --- /dev/null +++ b/packages/styleguide/src/lib/Foundations/ZIndex.mdx @@ -0,0 +1,75 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + +import { ComponentHeader, LinkTo, TokenTable } from '~styleguide/blocks'; + +import * as TABLES from './shared/elements'; + +export const parameters = { + title: 'Z-Index', + subtitle: + 'A single semantic scale for coordinating stacking order across Gamut.', + status: 'static', +}; + + + + + +Gamut exposes one semantic z-index scale, `zIndices`, so stacking order is coordinated by name +instead of by scattered magic numbers. Import it from `@codecademy/gamut-styles` and pass a token to +the `zIndex` prop: + +```tsx +import { zIndices } from '@codecademy/gamut-styles'; + +; +``` + +## The scale + +Values run in two bands. **In-flow** tokens (below `0`–`100`) order elements relative to their +siblings inside a normal stacking context. **Portal** tokens (`200`+) order elements that render +into a portal at `document.body` and compete at the page root. Portal values are spaced by 100. + + + +- **`underlay` → `foreground`** are in-flow: a decorative layer behind content, a neutral ground + that establishes a stacking context, and content raised in front (including sticky headers). +- **`portal` → `tooltip`** are the portal layers, ordered so the things that must never be clipped + (tooltips, toasts, popovers) sit above dialogs, which sit above side panels and the app bar. +- `portal` is the floor of the portal band and the default for `BodyPortal`, so portaled content + that omits a specific token still lands above page content. + +## Why it's a _soft_ scale + +Unlike hard token scales such as borderRadii or spacing — +where the prop is bound to the scale and **only** accepts token values — the `zIndex` prop is left +**numeric**. `zIndices` is just a numeric object you reference by name; the prop still accepts a raw +number. This is deliberate: + +- **Escape hatch.** Stacking occasionally needs a one-off value between two layers (e.g. a confirm + dialog nested inside a modal). You can pass a raw in-between number without leaving the system: + + ```tsx + ; // just above modal + ; // deliberate one-off + ``` + +- **Arithmetic.** Some components derive a relative value from a base (e.g. a tooltip's shadow sits + a step behind its body: `zIndices.foreground - 2`). A token-only string prop can't express that. + +Binding `zIndex` to the scale as a hard token would break both of the above (variance scaled props +reject raw numbers), so it stays soft. To keep it from sliding back into magic numbers, the +`gamut/no-raw-z-index` lint rule flags bare numeric z-index values and steers you to a token; a +genuine escape hatch uses an inline `eslint-disable` with a short justification. + +## Guidance + +- **Reach for a token first.** `zIndices.modal`, `zIndices.foreground`, etc. — the name documents + intent far better than a number. +- **In-flow vs. portal.** Use the in-flow tokens for stacking within a component; only portaled + content belongs on the `portal`+ band. +- **Escape hatch sparingly.** A raw number is fine for a genuine one-off, but prefer + `token ± n` so the relationship to the scale stays visible, and leave a comment. +- **Third-party widgets** (e.g. injected marketing/chat scripts) set their own z-index and are out + of Gamut's control; `tooltip` is the highest Gamut layer. diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index 1ec160156af..e1a838776cb 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -6,6 +6,7 @@ import { lxStudioColors, theme, trueColors, + zIndices as zIndicesTokens, } from '@codecademy/gamut-styles'; // eslint-disable-next-line gamut/import-paths import * as ALL_PROPS from '@codecademy/gamut-styles/src/variance/config'; @@ -438,6 +439,91 @@ export const borderRadii = { ], }; +// Representative components that use each token, linked to their Storybook stories. +// Story ids are the folder path under `src/lib` (Storybook auto-title). +const zIndexExamples: Record = { + underlay: [ + { label: 'Text', id: 'Typography/Text' }, + { label: 'Menu', id: 'Molecules/Menu' }, + ], + base: [ + { label: 'Tabs', id: 'Molecules/Tabs' }, + { label: 'Anchor', id: 'Typography/Anchor' }, + ], + foreground: [ + { label: 'Text', id: 'Typography/Text' }, + { label: 'Tabs', id: 'Molecules/Tabs' }, + { label: 'DataList', id: 'Organisms/Lists & Tables/DataList' }, + ], + flyout: [{ label: 'Flyout', id: 'Molecules/Flyout' }], + modal: [ + { label: 'Modal', id: 'Molecules/Modals/Modal' }, + { label: 'Dialog', id: 'Molecules/Modals/Dialog' }, + ], + popover: [ + { label: 'Popover', id: 'Molecules/Popover' }, + { label: 'SelectDropdown', id: 'Atoms/FormInputs/SelectDropdown' }, + ], + toaster: [{ label: 'Toaster', id: 'Molecules/Toasts/Toaster' }], + tooltip: [{ label: 'ToolTip', id: 'Molecules/Tips/ToolTip' }], +}; + +// Tokens with no single component example get a short note instead of links. +const zIndexNotes: Record = { + portal: 'BodyPortal default', + widget: 'Floating launchers (none in Gamut yet)', + appBar: 'App header / nav (app-owned)', +}; + +export const zIndices = { + // Object insertion order runs low → high (underlay … tooltip). + rows: Object.entries(zIndicesTokens).map(([id, value]) => ({ + id, + value, + })), + columns: [ + { ...PROP_COLUMN, name: 'Token' }, + { + ...PATH_COLUMN, + render: ({ id }: any) => zIndices.{id}, + }, + VALUE_COLUMN, + { + key: 'band', + name: 'Band', + size: 'lg', + render: ({ value }: any) => ( + {value >= zIndicesTokens.portal ? 'portal' : 'in-flow'} + ), + }, + { + key: 'usedBy', + name: 'Used by', + size: 'lg', + render: ({ id }: any) => { + const examples = zIndexExamples[id]; + if (!examples?.length) { + return {zIndexNotes[id] ?? '—'}; + } + return ( + <> + {examples.flatMap((example, i) => { + const link = ( + + {example.label} + + ); + return i === 0 + ? [link] + : [, , link]; + })} + + ); + }, + }, + ], +}; + export const LightModeTable = () => ( From 32cfd25581c1f433f7b4947d548ffa544e118fcf Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 15:56:55 -0400 Subject: [PATCH 05/10] linting errors --- .../src/lib/Foundations/shared/elements.tsx | 2 +- .../DataList/DataList.stories.tsx | 40 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index e1a838776cb..a74f1e51a82 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -509,7 +509,7 @@ export const zIndices = { <> {examples.flatMap((example, i) => { const link = ( - + {example.label} ); diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index f065ace0a80..27d0fbde385 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -101,26 +101,26 @@ const meta: Meta = { header: false, spacing: 'condensed', onRowSelect: () => {}, - // onRowExpand: () => {}, - // expandedContent: ({ row }) => ( - // - // - // - // ), + onRowExpand: () => {}, + expandedContent: ({ row }) => ( + + + + ), }, }; From 83c108a23581c1eb33b059a1baa025e5cd52516f Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 16:01:16 -0400 Subject: [PATCH 06/10] updated snapshots --- .../__snapshots__/theme.test.ts.snap | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap index 2769c9de387..c04ce5490db 100644 --- a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap +++ b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`themes admin - theme shape 1`] = ` { @@ -161,7 +161,7 @@ exports[`themes admin - theme shape 1`] = ` "portal": 200, "toaster": 800, "tooltip": 900, - "underlay": -1, + "underlay": -100, "widget": 300, }, }, @@ -280,7 +280,7 @@ exports[`themes admin - theme shape 1`] = ` "--zIndices-portal": 200, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, - "--zIndices-underlay": -1, + "--zIndices-underlay": -100, "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", @@ -714,7 +714,7 @@ exports[`themes core - theme shape 1`] = ` "portal": 200, "toaster": 800, "tooltip": 900, - "underlay": -1, + "underlay": -100, "widget": 300, }, }, @@ -833,7 +833,7 @@ exports[`themes core - theme shape 1`] = ` "--zIndices-portal": 200, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, - "--zIndices-underlay": -1, + "--zIndices-underlay": -100, "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", @@ -1270,7 +1270,7 @@ exports[`themes lxStudio - theme shape 1`] = ` "portal": 200, "toaster": 800, "tooltip": 900, - "underlay": -1, + "underlay": -100, "widget": 300, }, }, @@ -1392,7 +1392,7 @@ exports[`themes lxStudio - theme shape 1`] = ` "--zIndices-portal": 200, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, - "--zIndices-underlay": -1, + "--zIndices-underlay": -100, "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", @@ -1847,7 +1847,7 @@ exports[`themes percipio - theme shape 1`] = ` "portal": 200, "toaster": 800, "tooltip": 900, - "underlay": -1, + "underlay": -100, "widget": 300, }, }, @@ -1982,7 +1982,7 @@ exports[`themes percipio - theme shape 1`] = ` "--zIndices-portal": 200, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, - "--zIndices-underlay": -1, + "--zIndices-underlay": -100, "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", @@ -2504,7 +2504,7 @@ exports[`themes platform - theme shape 1`] = ` "portal": 200, "toaster": 800, "tooltip": 900, - "underlay": -1, + "underlay": -100, "widget": 300, }, }, @@ -2669,7 +2669,7 @@ exports[`themes platform - theme shape 1`] = ` "--zIndices-portal": 200, "--zIndices-toaster": 800, "--zIndices-tooltip": 900, - "--zIndices-underlay": -1, + "--zIndices-underlay": -100, "--zIndices-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", From 715f4f02410e18900234cc251c46d62a783409a6 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Wed, 8 Jul 2026 16:18:13 -0400 Subject: [PATCH 07/10] add version plan --- .../version-plan-1783541829799.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .nx/version-plans/version-plan-1783541829799.md diff --git a/.nx/version-plans/version-plan-1783541829799.md b/.nx/version-plans/version-plan-1783541829799.md new file mode 100644 index 00000000000..a4717424084 --- /dev/null +++ b/.nx/version-plans/version-plan-1783541829799.md @@ -0,0 +1,34 @@ +--- +eslint-plugin-gamut: major +gamut-styles: minor +gamut: major +gamut-kit: major +--- + +Add a semantic z-index scale (`zIndices`) and migrate Gamut off ad-hoc z-index values. + +**gamut-styles** + +- Add the `zIndices` token scale (exported object + theme scale with `--zIndices-*` CSS variables): + `underlay (-100)`, `base (0)`, `foreground (100)`, `portal (200)`, `widget (300)`, `appBar (400)`, + `flyout (500)`, `modal (600)`, `popover (700)`, `toaster (800)`, `tooltip (900)`. +- It is a _soft_ scale: the `zIndex` prop stays numeric, so tokens are used as `zIndex={zIndices.modal}` + and raw-number escape hatches / arithmetic (`zIndices.foreground - 2`) still work. +- `elements.headerZ` now aliases `zIndices.appBar` (was `15`). Prefer `zIndices.appBar` going forward. + +**gamut** + +- ⚠️ Behavior change — component stacking now uses the scale, which retunes several z-index values: + `Overlay`/`Modal`/`Dialog` → `modal (600)`, `Flyout` → `flyout (500)`, `Toaster` → `toaster (800)` + (fixes toasts previously stacking atPortal`default →`portal (200)`, sticky +`TableHeader`→`foreground (100)`, and many in-flow values → `underlay`/`base`/`foreground`. +- `SelectDropdown`'s options menu now portals to `document.body` (a true popover at `zIndices.popover`), + so it renders above sticky headers and modal content instead of being clipped. +- No public API is removed or retyped; the `zIndex` prop still accepts numbers. + +**eslint-plugin-gamut** + +- Add rule `gamut/no-raw-z-index`, enabled as `error` in the recommended config. It flags raw numeric + z-index literals in style objects and JSX props and steers to `zIndices` tokens; token references and + arithmetic are allowed, and deliberate escape hatches use an inline `eslint-disable`. Major because + the new error-level recommended rule can fail existing lint in consumers until raw z-index is migrated. From 2b8203892f5dc69b27b7abe39870b4c2286d02a5 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 10 Jul 2026 16:53:33 -0400 Subject: [PATCH 08/10] update docs to reflect changes --- .../gamut-styles/src/variables/zIndices.ts | 38 ++++++++-------- packages/gamut/src/BodyPortal/index.tsx | 4 +- packages/gamut/src/Toaster/index.tsx | 2 +- .../styleguide/src/lib/Foundations/ZIndex.mdx | 45 +++++-------------- .../src/lib/Foundations/shared/elements.tsx | 17 +++---- 5 files changed, 39 insertions(+), 67 deletions(-) diff --git a/packages/gamut-styles/src/variables/zIndices.ts b/packages/gamut-styles/src/variables/zIndices.ts index 873d947a8ac..eecd6894770 100644 --- a/packages/gamut-styles/src/variables/zIndices.ts +++ b/packages/gamut-styles/src/variables/zIndices.ts @@ -1,14 +1,14 @@ /** * Semantic z-index scale. A single scale covers both in-flow layers (low values) and - * portal layers (≥100). Every z-index in Gamut should reference a token here rather than a + * portal layers (≥200). Every z-index in Gamut should reference a token here rather than a * magic number. * * The `zIndex` prop is intentionally left numeric/unscaled, and this object is numeric, so * tokens are used as `zIndex={zIndices.modal}`. That preserves the escape hatch (a raw - * in-between number, e.g. `zIndex={605}`) and arithmetic on tokens (e.g. `zIndices.foreground - 2`). + * in-between number, e.g. `zIndex={550}`) and arithmetic on tokens (e.g. `zIndices.foreground - 2`). * - * Portal-band values are spaced by 100 so in-between escape-hatch numbers are available. - * `portal` (200) is the floor of the portal band and the default for `BodyPortal`. + * Values are spaced by 100 so in-between escape-hatch numbers are available. `floating` (200) is + * the floor of the portal band and the default for `BodyPortal`. * * @remarks PLEASE talk to web platform before adding new z-index tokens. */ @@ -20,24 +20,26 @@ export const zIndices = { /** * The raised in-flow layer: an element in front of what sits/scrolls behind it, but below * all portal overlays. Covers content lifted above an `underlay` (e.g. text over its - * underline) and sticky content headers (e.g. a sticky table `thead`). Set to 100 so it - * clears common ad-hoc low z-index values. + * underline) and sticky content headers (e.g. a sticky table `thead`). */ foreground: 100, - /** Portal floor / `BodyPortal` default — un-tokenized portal content lands here. */ - portal: 200, - /** Persistent floating page furniture at rest (e.g. an AI chat launcher, help bubble). */ - widget: 300, + /** + * Portal floor: the default for `BodyPortal`, and the layer for persistent floating page + * furniture at rest (e.g. an AI chat launcher, help bubble). Above page content, below the + * app nav and all overlays. + */ + floating: 200, /** Global app header / nav bar. Aliased by the legacy `elements.headerZ` constant. */ - appBar: 400, + appBar: 300, /** Portaled side panel (the `Flyout` component = `Drawer` inside `Overlay`). */ - flyout: 500, + flyout: 400, /** `Overlay`, `Modal`, and `Dialog` (they share one portal primitive). */ - modal: 600, + modal: 500, /** Portal-mode `Popover` and the portaled `SelectDropdown` menu — above modal. */ - popover: 700, - /** Toasts / notifications — visible above modals. */ - toaster: 800, - /** Floating tooltips (`FloatingTip`) — highest, so they are never clipped. */ - tooltip: 900, + popover: 600, + /** + * Top-most transient overlays that must never be clipped: floating tooltips (`FloatingTip`) + * and toasts / notifications (`Toaster`). Highest layer — nothing in Gamut sits above it. + */ + topmost: 700, } as const; diff --git a/packages/gamut/src/BodyPortal/index.tsx b/packages/gamut/src/BodyPortal/index.tsx index 34309f27739..e6c779a61d2 100644 --- a/packages/gamut/src/BodyPortal/index.tsx +++ b/packages/gamut/src/BodyPortal/index.tsx @@ -26,7 +26,7 @@ interface BodyPortalProps { /** * Stacking layer for the portaled content. Pass a `zIndices` token * (e.g. `zIndices.modal`) or a raw number as an escape hatch. Defaults to - * `zIndices.portal` — the floor of the portal band — so un-tokenized portal content + * `zIndices.floating` — the floor of the portal band — so un-tokenized portal content * stays above local page content instead of silently landing at a low value. */ zIndex?: number; @@ -34,7 +34,7 @@ interface BodyPortalProps { export const BodyPortal: React.FC> = ({ children, - zIndex = zIndices.portal, + zIndex = zIndices.floating, }) => { const [ready, setReady] = useState(false); const mode = useCurrentMode(); diff --git a/packages/gamut/src/Toaster/index.tsx b/packages/gamut/src/Toaster/index.tsx index 00c653c1614..a3320fa12d2 100644 --- a/packages/gamut/src/Toaster/index.tsx +++ b/packages/gamut/src/Toaster/index.tsx @@ -26,7 +26,7 @@ export const Toaster: React.FC = ({ colorMode = 'light', }) => { return ( - + diff --git a/packages/styleguide/src/lib/Foundations/ZIndex.mdx b/packages/styleguide/src/lib/Foundations/ZIndex.mdx index d597f7f639f..308d78538be 100644 --- a/packages/styleguide/src/lib/Foundations/ZIndex.mdx +++ b/packages/styleguide/src/lib/Foundations/ZIndex.mdx @@ -15,7 +15,9 @@ export const parameters = { -Gamut exposes one semantic z-index scale, `zIndices`, so stacking order is coordinated by name +z-index should only be used when necessary to override the default stacking order. When possible, use the default stacking order and avoid using z-index. + +To standardize z-index values across the application, Gamut exposes one semantic z-index scale, `zIndices`, so stacking order is coordinated by name instead of by scattered magic numbers. Import it from `@codecademy/gamut-styles` and pass a token to the `zIndex` prop: @@ -27,49 +29,24 @@ import { zIndices } from '@codecademy/gamut-styles'; ## The scale -Values run in two bands. **In-flow** tokens (below `0`–`100`) order elements relative to their -siblings inside a normal stacking context. **Portal** tokens (`200`+) order elements that render -into a portal at `document.body` and compete at the page root. Portal values are spaced by 100. - -- **`underlay` → `foreground`** are in-flow: a decorative layer behind content, a neutral ground - that establishes a stacking context, and content raised in front (including sticky headers). -- **`portal` → `tooltip`** are the portal layers, ordered so the things that must never be clipped - (tooltips, toasts, popovers) sit above dialogs, which sit above side panels and the app bar. -- `portal` is the floor of the portal band and the default for `BodyPortal`, so portaled content - that omits a specific token still lands above page content. - -## Why it's a _soft_ scale - -Unlike hard token scales such as borderRadii or spacing — -where the prop is bound to the scale and **only** accepts token values — the `zIndex` prop is left -**numeric**. `zIndices` is just a numeric object you reference by name; the prop still accepts a raw -number. This is deliberate: +## Custom z-index values -- **Escape hatch.** Stacking occasionally needs a one-off value between two layers (e.g. a confirm - dialog nested inside a modal). You can pass a raw in-between number without leaving the system: +The `zIndex` prop is left **numeric**. `zIndices` is just a numeric object you reference by name; the prop still accepts a raw number. This is deliberate in situations where you need to set a z-index that is not part of the scale. - ```tsx - ; // just above modal - ; // deliberate one-off - ``` - -- **Arithmetic.** Some components derive a relative value from a base (e.g. a tooltip's shadow sits - a step behind its body: `zIndices.foreground - 2`). A token-only string prop can't express that. +```tsx +; // just above modal +; // deliberate one-off +``` -Binding `zIndex` to the scale as a hard token would break both of the above (variance scaled props -reject raw numbers), so it stays soft. To keep it from sliding back into magic numbers, the -`gamut/no-raw-z-index` lint rule flags bare numeric z-index values and steers you to a token; a -genuine escape hatch uses an inline `eslint-disable` with a short justification. +These situations should be rare, but when they do arise, you can use the scale as a reference point. ## Guidance - **Reach for a token first.** `zIndices.modal`, `zIndices.foreground`, etc. — the name documents intent far better than a number. -- **In-flow vs. portal.** Use the in-flow tokens for stacking within a component; only portaled - content belongs on the `portal`+ band. - **Escape hatch sparingly.** A raw number is fine for a genuine one-off, but prefer `token ± n` so the relationship to the scale stays visible, and leave a comment. - **Third-party widgets** (e.g. injected marketing/chat scripts) set their own z-index and are out - of Gamut's control; `tooltip` is the highest Gamut layer. + of Gamut's control; `topmost` is the highest Gamut layer. diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index a74f1e51a82..1bcd9f8e998 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -464,14 +464,15 @@ const zIndexExamples: Record = { { label: 'Popover', id: 'Molecules/Popover' }, { label: 'SelectDropdown', id: 'Atoms/FormInputs/SelectDropdown' }, ], - toaster: [{ label: 'Toaster', id: 'Molecules/Toasts/Toaster' }], - tooltip: [{ label: 'ToolTip', id: 'Molecules/Tips/ToolTip' }], + topmost: [ + { label: 'ToolTip', id: 'Molecules/Tips/ToolTip' }, + { label: 'Toaster', id: 'Molecules/Toasts/Toaster' }, + ], }; // Tokens with no single component example get a short note instead of links. const zIndexNotes: Record = { - portal: 'BodyPortal default', - widget: 'Floating launchers (none in Gamut yet)', + floating: 'BodyPortal default; floating launchers (e.g. AI chat button)', appBar: 'App header / nav (app-owned)', }; @@ -488,14 +489,6 @@ export const zIndices = { render: ({ id }: any) => zIndices.{id}, }, VALUE_COLUMN, - { - key: 'band', - name: 'Band', - size: 'lg', - render: ({ value }: any) => ( - {value >= zIndicesTokens.portal ? 'portal' : 'in-flow'} - ), - }, { key: 'usedBy', name: 'Used by', From a7d0445fa6316d3f9cf13dc1e3cee80e144faa14 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 13 Jul 2026 10:34:17 -0400 Subject: [PATCH 09/10] updated naming to zIndexes --- .../version-plan-1783541829799.md | 14 +- .../src/no-raw-z-index.test.ts | 8 +- .../eslint-plugin-gamut/src/no-raw-z-index.ts | 4 +- .../__snapshots__/theme.test.ts.snap | 240 +++++++++--------- packages/gamut-styles/src/themes/core.ts | 6 +- .../gamut-styles/src/variables/elements.ts | 6 +- packages/gamut-styles/src/variables/index.ts | 2 +- .../variables/{zIndices.ts => zIndexes.ts} | 9 +- packages/gamut-styles/src/variance/config.ts | 2 +- packages/gamut/src/Anchor/index.tsx | 4 +- .../gamut/src/BarChart/layout/GridLines.tsx | 4 +- packages/gamut/src/BodyPortal/index.tsx | 10 +- packages/gamut/src/Box/props.ts | 4 +- packages/gamut/src/Button/shared/styles.ts | 4 +- packages/gamut/src/DataList/EmptyRows.tsx | 4 +- .../Calendar/CalendarWrapper.tsx | 4 +- .../Calendar/utils/elements.tsx | 4 +- packages/gamut/src/Flyout/index.tsx | 4 +- .../gamut/src/Form/SelectDropdown/styles.ts | 4 +- packages/gamut/src/List/TableHeader.tsx | 4 +- packages/gamut/src/List/elements.tsx | 10 +- packages/gamut/src/Menu/elements.tsx | 8 +- packages/gamut/src/Overlay/index.tsx | 10 +- .../src/PatternBackdrop/PatternBackdrop.tsx | 6 +- packages/gamut/src/Popover/elements.tsx | 6 +- packages/gamut/src/Popover/styles/base.ts | 4 +- packages/gamut/src/Popover/styles/variants.ts | 4 +- .../src/PopoverContainer/PopoverContainer.tsx | 4 +- packages/gamut/src/Tabs/TabButton.tsx | 6 +- packages/gamut/src/Tabs/Tabs.tsx | 6 +- packages/gamut/src/Tabs/styles.tsx | 4 +- .../gamut/src/Tip/PreviewTip/elements.tsx | 4 +- packages/gamut/src/Tip/shared/InlineTip.tsx | 4 +- packages/gamut/src/Tip/shared/elements.tsx | 4 +- packages/gamut/src/Toaster/index.tsx | 4 +- packages/gamut/src/Typography/Text.tsx | 6 +- .../styleguide/src/lib/Foundations/ZIndex.mdx | 14 +- .../src/lib/Foundations/shared/elements.tsx | 8 +- 38 files changed, 226 insertions(+), 227 deletions(-) rename packages/gamut-styles/src/variables/{zIndices.ts => zIndexes.ts} (86%) diff --git a/.nx/version-plans/version-plan-1783541829799.md b/.nx/version-plans/version-plan-1783541829799.md index a4717424084..e9a5bba8aaa 100644 --- a/.nx/version-plans/version-plan-1783541829799.md +++ b/.nx/version-plans/version-plan-1783541829799.md @@ -5,16 +5,16 @@ gamut: major gamut-kit: major --- -Add a semantic z-index scale (`zIndices`) and migrate Gamut off ad-hoc z-index values. +Add a semantic z-index scale (`zIndexes`) and migrate Gamut off ad-hoc z-index values. **gamut-styles** -- Add the `zIndices` token scale (exported object + theme scale with `--zIndices-*` CSS variables): +- Add the `zIndexes` token scale (exported object + theme scale with `--zIndexes-*` CSS variables): `underlay (-100)`, `base (0)`, `foreground (100)`, `portal (200)`, `widget (300)`, `appBar (400)`, `flyout (500)`, `modal (600)`, `popover (700)`, `toaster (800)`, `tooltip (900)`. -- It is a _soft_ scale: the `zIndex` prop stays numeric, so tokens are used as `zIndex={zIndices.modal}` - and raw-number escape hatches / arithmetic (`zIndices.foreground - 2`) still work. -- `elements.headerZ` now aliases `zIndices.appBar` (was `15`). Prefer `zIndices.appBar` going forward. +- It is a _soft_ scale: the `zIndex` prop stays numeric, so tokens are used as `zIndex={zIndexes.modal}` + and raw-number escape hatches / arithmetic (`zIndexes.foreground - 2`) still work. +- `elements.headerZ` now aliases `zIndexes.appBar` (was `15`). Prefer `zIndexes.appBar` going forward. **gamut** @@ -22,13 +22,13 @@ Add a semantic z-index scale (`zIndices`) and migrate Gamut off ad-hoc z-index v `Overlay`/`Modal`/`Dialog` → `modal (600)`, `Flyout` → `flyout (500)`, `Toaster` → `toaster (800)` (fixes toasts previously stacking atPortal`default →`portal (200)`, sticky `TableHeader`→`foreground (100)`, and many in-flow values → `underlay`/`base`/`foreground`. -- `SelectDropdown`'s options menu now portals to `document.body` (a true popover at `zIndices.popover`), +- `SelectDropdown`'s options menu now portals to `document.body` (a true popover at `zIndexes.popover`), so it renders above sticky headers and modal content instead of being clipped. - No public API is removed or retyped; the `zIndex` prop still accepts numbers. **eslint-plugin-gamut** - Add rule `gamut/no-raw-z-index`, enabled as `error` in the recommended config. It flags raw numeric - z-index literals in style objects and JSX props and steers to `zIndices` tokens; token references and + z-index literals in style objects and JSX props and steers to `zIndexes` tokens; token references and arithmetic are allowed, and deliberate escape hatches use an inline `eslint-disable`. Major because the new error-level recommended rule can fail existing lint in consumers until raw z-index is migrated. diff --git a/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts b/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts index 75da9221e7b..4a0180cf5fb 100644 --- a/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts +++ b/packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts @@ -14,11 +14,11 @@ const ruleTester = new ESLintUtils.RuleTester({ ruleTester.run('no-raw-z-index', rule, { valid: [ // Semantic tokens are the expected usage. - `const styles = { zIndex: zIndices.modal };`, - `;`, + `const styles = { zIndex: zIndexes.modal };`, + `;`, // Arithmetic on a token is allowed (e.g. Tip's shadow). - `const styles = { zIndex: zIndices.foreground - 2 };`, - `;`, + `const styles = { zIndex: zIndexes.foreground - 2 };`, + `;`, // Variables / non-literal expressions are not flagged. `;`, `const styles = { zIndex };`, diff --git a/packages/eslint-plugin-gamut/src/no-raw-z-index.ts b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts index 3a0ca1e55eb..77da92ae299 100644 --- a/packages/eslint-plugin-gamut/src/no-raw-z-index.ts +++ b/packages/eslint-plugin-gamut/src/no-raw-z-index.ts @@ -48,12 +48,12 @@ export default createRule({ meta: { docs: { description: - 'Discourage raw numeric z-index values which can lead to z-index stacking issues and encourage usage of semantic tokens from the `zIndices` scale.', + 'Discourage raw numeric z-index values which can lead to z-index stacking issues and encourage usage of semantic tokens from the `zIndexes` scale.', recommended: 'error', }, messages: { noRawZIndex: - 'Semantic tokens from the `zIndices` scale (e.g. `zIndices.modal`) are recommendedinstead of a raw z-index number. For a deliberate in-between value, disable this rule inline with a justifying comment.', + 'Semantic tokens from the `zIndexes` scale (e.g. `zIndexes.modal`) are recommendedinstead of a raw z-index number. For a deliberate in-between value, disable this rule inline with a justifying comment.', }, type: 'suggestion', schema: [], diff --git a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap index c04ce5490db..ad4b27ca511 100644 --- a/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap +++ b/packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap @@ -151,7 +151,7 @@ exports[`themes admin - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, - "zIndices": { + "zIndexes": { "appBar": 400, "base": 0, "flyout": 500, @@ -271,17 +271,17 @@ exports[`themes admin - theme shape 1`] = ` "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", "--elements-headerZ": 400, - "--zIndices-appBar": 400, - "--zIndices-base": 0, - "--zIndices-flyout": 500, - "--zIndices-foreground": 100, - "--zIndices-modal": 600, - "--zIndices-popover": 700, - "--zIndices-portal": 200, - "--zIndices-toaster": 800, - "--zIndices-tooltip": 900, - "--zIndices-underlay": -100, - "--zIndices-widget": 300, + "--zIndexes-appBar": 400, + "--zIndexes-base": 0, + "--zIndexes-flyout": 500, + "--zIndexes-foreground": 100, + "--zIndexes-modal": 600, + "--zIndexes-popover": 700, + "--zIndexes-portal": 200, + "--zIndexes-toaster": 800, + "--zIndexes-tooltip": 900, + "--zIndexes-underlay": -100, + "--zIndexes-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -537,18 +537,18 @@ monospace", "8": "0.5rem", "96": "6rem", }, - "zIndices": { - "appBar": "var(--zIndices-appBar)", - "base": "var(--zIndices-base)", - "flyout": "var(--zIndices-flyout)", - "foreground": "var(--zIndices-foreground)", - "modal": "var(--zIndices-modal)", - "popover": "var(--zIndices-popover)", - "portal": "var(--zIndices-portal)", - "toaster": "var(--zIndices-toaster)", - "tooltip": "var(--zIndices-tooltip)", - "underlay": "var(--zIndices-underlay)", - "widget": "var(--zIndices-widget)", + "zIndexes": { + "appBar": "var(--zIndexes-appBar)", + "base": "var(--zIndexes-base)", + "flyout": "var(--zIndexes-flyout)", + "foreground": "var(--zIndexes-foreground)", + "modal": "var(--zIndexes-modal)", + "popover": "var(--zIndexes-popover)", + "portal": "var(--zIndexes-portal)", + "toaster": "var(--zIndexes-toaster)", + "tooltip": "var(--zIndexes-tooltip)", + "underlay": "var(--zIndexes-underlay)", + "widget": "var(--zIndexes-widget)", }, } `; @@ -704,7 +704,7 @@ exports[`themes core - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, - "zIndices": { + "zIndexes": { "appBar": 400, "base": 0, "flyout": 500, @@ -824,17 +824,17 @@ exports[`themes core - theme shape 1`] = ` "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", "--elements-headerZ": 400, - "--zIndices-appBar": 400, - "--zIndices-base": 0, - "--zIndices-flyout": 500, - "--zIndices-foreground": 100, - "--zIndices-modal": 600, - "--zIndices-popover": 700, - "--zIndices-portal": 200, - "--zIndices-toaster": 800, - "--zIndices-tooltip": 900, - "--zIndices-underlay": -100, - "--zIndices-widget": 300, + "--zIndexes-appBar": 400, + "--zIndexes-base": 0, + "--zIndexes-flyout": 500, + "--zIndexes-foreground": 100, + "--zIndexes-modal": 600, + "--zIndexes-popover": 700, + "--zIndexes-portal": 200, + "--zIndexes-toaster": 800, + "--zIndexes-tooltip": 900, + "--zIndexes-underlay": -100, + "--zIndexes-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -1090,18 +1090,18 @@ monospace", "8": "0.5rem", "96": "6rem", }, - "zIndices": { - "appBar": "var(--zIndices-appBar)", - "base": "var(--zIndices-base)", - "flyout": "var(--zIndices-flyout)", - "foreground": "var(--zIndices-foreground)", - "modal": "var(--zIndices-modal)", - "popover": "var(--zIndices-popover)", - "portal": "var(--zIndices-portal)", - "toaster": "var(--zIndices-toaster)", - "tooltip": "var(--zIndices-tooltip)", - "underlay": "var(--zIndices-underlay)", - "widget": "var(--zIndices-widget)", + "zIndexes": { + "appBar": "var(--zIndexes-appBar)", + "base": "var(--zIndexes-base)", + "flyout": "var(--zIndexes-flyout)", + "foreground": "var(--zIndexes-foreground)", + "modal": "var(--zIndexes-modal)", + "popover": "var(--zIndexes-popover)", + "portal": "var(--zIndexes-portal)", + "toaster": "var(--zIndexes-toaster)", + "tooltip": "var(--zIndexes-tooltip)", + "underlay": "var(--zIndexes-underlay)", + "widget": "var(--zIndexes-widget)", }, } `; @@ -1260,7 +1260,7 @@ exports[`themes lxStudio - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, - "zIndices": { + "zIndexes": { "appBar": 400, "base": 0, "flyout": 500, @@ -1383,17 +1383,17 @@ exports[`themes lxStudio - theme shape 1`] = ` "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", "--elements-headerZ": 400, - "--zIndices-appBar": 400, - "--zIndices-base": 0, - "--zIndices-flyout": 500, - "--zIndices-foreground": 100, - "--zIndices-modal": 600, - "--zIndices-popover": 700, - "--zIndices-portal": 200, - "--zIndices-toaster": 800, - "--zIndices-tooltip": 900, - "--zIndices-underlay": -100, - "--zIndices-widget": 300, + "--zIndexes-appBar": 400, + "--zIndexes-base": 0, + "--zIndexes-flyout": 500, + "--zIndexes-foreground": 100, + "--zIndexes-modal": 600, + "--zIndexes-popover": 700, + "--zIndexes-portal": 200, + "--zIndexes-toaster": 800, + "--zIndexes-tooltip": 900, + "--zIndexes-underlay": -100, + "--zIndexes-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -1654,18 +1654,18 @@ monospace", "8": "0.5rem", "96": "6rem", }, - "zIndices": { - "appBar": "var(--zIndices-appBar)", - "base": "var(--zIndices-base)", - "flyout": "var(--zIndices-flyout)", - "foreground": "var(--zIndices-foreground)", - "modal": "var(--zIndices-modal)", - "popover": "var(--zIndices-popover)", - "portal": "var(--zIndices-portal)", - "toaster": "var(--zIndices-toaster)", - "tooltip": "var(--zIndices-tooltip)", - "underlay": "var(--zIndices-underlay)", - "widget": "var(--zIndices-widget)", + "zIndexes": { + "appBar": "var(--zIndexes-appBar)", + "base": "var(--zIndexes-base)", + "flyout": "var(--zIndexes-flyout)", + "foreground": "var(--zIndexes-foreground)", + "modal": "var(--zIndexes-modal)", + "popover": "var(--zIndexes-popover)", + "portal": "var(--zIndexes-portal)", + "toaster": "var(--zIndexes-toaster)", + "tooltip": "var(--zIndexes-tooltip)", + "underlay": "var(--zIndexes-underlay)", + "widget": "var(--zIndexes-widget)", }, } `; @@ -1837,7 +1837,7 @@ exports[`themes percipio - theme shape 1`] = ` "text-secondary": "rgba(34, 35, 37, 0.75)", }, }, - "zIndices": { + "zIndexes": { "appBar": 400, "base": 0, "flyout": 500, @@ -1973,17 +1973,17 @@ exports[`themes percipio - theme shape 1`] = ` "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", "--elements-headerZ": 400, - "--zIndices-appBar": 400, - "--zIndices-base": 0, - "--zIndices-flyout": 500, - "--zIndices-foreground": 100, - "--zIndices-modal": 600, - "--zIndices-popover": 700, - "--zIndices-portal": 200, - "--zIndices-toaster": 800, - "--zIndices-tooltip": 900, - "--zIndices-underlay": -100, - "--zIndices-widget": 300, + "--zIndexes-appBar": 400, + "--zIndexes-base": 0, + "--zIndexes-flyout": 500, + "--zIndexes-foreground": 100, + "--zIndexes-modal": 600, + "--zIndexes-popover": 700, + "--zIndexes-portal": 200, + "--zIndexes-toaster": 800, + "--zIndexes-tooltip": 900, + "--zIndexes-underlay": -100, + "--zIndexes-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -2255,18 +2255,18 @@ sans-serif", "8": "0.5rem", "96": "6rem", }, - "zIndices": { - "appBar": "var(--zIndices-appBar)", - "base": "var(--zIndices-base)", - "flyout": "var(--zIndices-flyout)", - "foreground": "var(--zIndices-foreground)", - "modal": "var(--zIndices-modal)", - "popover": "var(--zIndices-popover)", - "portal": "var(--zIndices-portal)", - "toaster": "var(--zIndices-toaster)", - "tooltip": "var(--zIndices-tooltip)", - "underlay": "var(--zIndices-underlay)", - "widget": "var(--zIndices-widget)", + "zIndexes": { + "appBar": "var(--zIndexes-appBar)", + "base": "var(--zIndexes-base)", + "flyout": "var(--zIndexes-flyout)", + "foreground": "var(--zIndexes-foreground)", + "modal": "var(--zIndexes-modal)", + "popover": "var(--zIndexes-popover)", + "portal": "var(--zIndexes-portal)", + "toaster": "var(--zIndexes-toaster)", + "tooltip": "var(--zIndexes-tooltip)", + "underlay": "var(--zIndexes-underlay)", + "widget": "var(--zIndexes-widget)", }, } `; @@ -2494,7 +2494,7 @@ exports[`themes platform - theme shape 1`] = ` "text-secondary": "rgba(16,22,47,0.75)", }, }, - "zIndices": { + "zIndexes": { "appBar": 400, "base": 0, "flyout": 500, @@ -2660,17 +2660,17 @@ exports[`themes platform - theme shape 1`] = ` "--color-yellow-900": "#211B00", "--elements-headerHeight": "4rem", "--elements-headerZ": 400, - "--zIndices-appBar": 400, - "--zIndices-base": 0, - "--zIndices-flyout": 500, - "--zIndices-foreground": 100, - "--zIndices-modal": 600, - "--zIndices-popover": 700, - "--zIndices-portal": 200, - "--zIndices-toaster": 800, - "--zIndices-tooltip": 900, - "--zIndices-underlay": -100, - "--zIndices-widget": 300, + "--zIndexes-appBar": 400, + "--zIndexes-base": 0, + "--zIndexes-flyout": 500, + "--zIndexes-foreground": 100, + "--zIndexes-modal": 600, + "--zIndexes-popover": 700, + "--zIndexes-portal": 200, + "--zIndexes-toaster": 800, + "--zIndexes-tooltip": 900, + "--zIndexes-underlay": -100, + "--zIndexes-widget": 300, "@media only screen and (min-width: 1024px)": { "--elements-headerHeight": "5rem", }, @@ -3024,18 +3024,18 @@ monospace", "8": "0.5rem", "96": "6rem", }, - "zIndices": { - "appBar": "var(--zIndices-appBar)", - "base": "var(--zIndices-base)", - "flyout": "var(--zIndices-flyout)", - "foreground": "var(--zIndices-foreground)", - "modal": "var(--zIndices-modal)", - "popover": "var(--zIndices-popover)", - "portal": "var(--zIndices-portal)", - "toaster": "var(--zIndices-toaster)", - "tooltip": "var(--zIndices-tooltip)", - "underlay": "var(--zIndices-underlay)", - "widget": "var(--zIndices-widget)", + "zIndexes": { + "appBar": "var(--zIndexes-appBar)", + "base": "var(--zIndexes-base)", + "flyout": "var(--zIndexes-flyout)", + "foreground": "var(--zIndexes-foreground)", + "modal": "var(--zIndexes-modal)", + "popover": "var(--zIndexes-popover)", + "portal": "var(--zIndexes-portal)", + "toaster": "var(--zIndexes-toaster)", + "tooltip": "var(--zIndexes-tooltip)", + "underlay": "var(--zIndexes-underlay)", + "widget": "var(--zIndexes-widget)", }, } `; diff --git a/packages/gamut-styles/src/themes/core.ts b/packages/gamut-styles/src/themes/core.ts index eb4457cdbea..e41a089abea 100644 --- a/packages/gamut-styles/src/themes/core.ts +++ b/packages/gamut-styles/src/themes/core.ts @@ -11,7 +11,7 @@ import { lineHeight, mediaQueries, spacing, - zIndices, + zIndexes, } from '../variables'; /** @@ -29,7 +29,7 @@ export const coreTheme = createTheme({ fontWeight, spacing, elements, - zIndices, + zIndexes, }) .addColors(corePalette) .addColorModes('light', { @@ -143,7 +143,7 @@ export const coreTheme = createTheme({ 2: `2px solid ${colors['border-primary']}`, })) .createScaleVariables('elements') - .createScaleVariables('zIndices') + .createScaleVariables('zIndexes') .addName('core') .build(); diff --git a/packages/gamut-styles/src/variables/elements.ts b/packages/gamut-styles/src/variables/elements.ts index 387199e0f28..c9e39f7a21e 100644 --- a/packages/gamut-styles/src/variables/elements.ts +++ b/packages/gamut-styles/src/variables/elements.ts @@ -1,12 +1,12 @@ -import { zIndices } from './zIndices'; +import { zIndexes } from './zIndexes'; export const elements = { headerHeight: { base: '4rem', md: '5rem' }, /** - * z-index for global page headers. Aliases the `appBar` token from the `zIndices` + * z-index for global page headers. Aliases the `appBar` token from the `zIndexes` * scale so consumers still reading `elements.headerZ` stay in sync with the scale. * Prefer `zIndex="appBar"` in new code. */ - headerZ: zIndices.appBar, + headerZ: zIndexes.appBar, } as const; diff --git a/packages/gamut-styles/src/variables/index.ts b/packages/gamut-styles/src/variables/index.ts index 06ad877133d..5fd8de5cd3e 100644 --- a/packages/gamut-styles/src/variables/index.ts +++ b/packages/gamut-styles/src/variables/index.ts @@ -5,4 +5,4 @@ export * from './responsive'; export * from './spacing'; export * from './timing'; export * from './typography'; -export * from './zIndices'; +export * from './zIndexes'; diff --git a/packages/gamut-styles/src/variables/zIndices.ts b/packages/gamut-styles/src/variables/zIndexes.ts similarity index 86% rename from packages/gamut-styles/src/variables/zIndices.ts rename to packages/gamut-styles/src/variables/zIndexes.ts index eecd6894770..823e1f3a035 100644 --- a/packages/gamut-styles/src/variables/zIndices.ts +++ b/packages/gamut-styles/src/variables/zIndexes.ts @@ -1,18 +1,17 @@ /** - * Semantic z-index scale. A single scale covers both in-flow layers (low values) and - * portal layers (≥200). Every z-index in Gamut should reference a token here rather than a + * Semantic z-index scale. Every z-index in Gamut should reference a token here rather than a * magic number. * * The `zIndex` prop is intentionally left numeric/unscaled, and this object is numeric, so - * tokens are used as `zIndex={zIndices.modal}`. That preserves the escape hatch (a raw - * in-between number, e.g. `zIndex={550}`) and arithmetic on tokens (e.g. `zIndices.foreground - 2`). + * tokens are used as `zIndex={zIndexes.modal}`. That preserves the escape hatch (a raw + * in-between number, e.g. `zIndex={550}`) and arithmetic on tokens (e.g. `zIndexes.foreground - 2`). * * Values are spaced by 100 so in-between escape-hatch numbers are available. `floating` (200) is * the floor of the portal band and the default for `BodyPortal`. * * @remarks PLEASE talk to web platform before adding new z-index tokens. */ -export const zIndices = { +export const zIndexes = { /** Decorative layer behind content (underlines, backdrops, shadows). */ underlay: -100, /** Ground layer — establishes a local stacking context without lifting above siblings. */ diff --git a/packages/gamut-styles/src/variance/config.ts b/packages/gamut-styles/src/variance/config.ts index 1cacdaef3f1..7513c4698dc 100644 --- a/packages/gamut-styles/src/variance/config.ts +++ b/packages/gamut-styles/src/variance/config.ts @@ -330,7 +330,7 @@ export const positioning = { }, // Intentionally unscaled: scaled variance props are token-only and reject raw numbers, // which would break the numeric escape hatch and Tip's z-index arithmetic. Use the numeric - // `zIndices` token object instead (e.g. zIndex={zIndices.modal}). + // `zIndexes` token object instead (e.g. zIndex={zIndexes.modal}). zIndex: { property: 'zIndex' }, opacity: { property: 'opacity' }, } as const; diff --git a/packages/gamut/src/Anchor/index.tsx b/packages/gamut/src/Anchor/index.tsx index b65906656eb..385da974858 100644 --- a/packages/gamut/src/Anchor/index.tsx +++ b/packages/gamut/src/Anchor/index.tsx @@ -2,7 +2,7 @@ import { styledOptions, system, variant, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -26,7 +26,7 @@ const outlineFocusVisible = { border: 2, borderColor: 'primary', opacity: 0, - zIndex: zIndices.base, + zIndex: zIndexes.base, }, [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { diff --git a/packages/gamut/src/BarChart/layout/GridLines.tsx b/packages/gamut/src/BarChart/layout/GridLines.tsx index bf1c9718c98..0444494780b 100644 --- a/packages/gamut/src/BarChart/layout/GridLines.tsx +++ b/packages/gamut/src/BarChart/layout/GridLines.tsx @@ -1,4 +1,4 @@ -import { css, zIndices } from '@codecademy/gamut-styles'; +import { css, zIndexes } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import React, { useMemo } from 'react'; @@ -12,7 +12,7 @@ const GridLineWrapper = styled(Box)( inset: 0, pointerEvents: 'none', position: 'absolute', - zIndex: zIndices.base, + zIndex: zIndexes.base, }) ); diff --git a/packages/gamut/src/BodyPortal/index.tsx b/packages/gamut/src/BodyPortal/index.tsx index e6c779a61d2..6fef2ba4008 100644 --- a/packages/gamut/src/BodyPortal/index.tsx +++ b/packages/gamut/src/BodyPortal/index.tsx @@ -2,7 +2,7 @@ import { ColorMode, system, useCurrentMode, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useState } from 'react'; @@ -24,9 +24,9 @@ const PortalWrapper = styled interface BodyPortalProps { /** - * Stacking layer for the portaled content. Pass a `zIndices` token - * (e.g. `zIndices.modal`) or a raw number as an escape hatch. Defaults to - * `zIndices.floating` — the floor of the portal band — so un-tokenized portal content + * Stacking layer for the portaled content. Pass a `zIndexes` token + * (e.g. `zIndexes.modal`) or a raw number as an escape hatch. Defaults to + * `zIndexes.floating` — the floor of the portal band — so un-tokenized portal content * stays above local page content instead of silently landing at a low value. */ zIndex?: number; @@ -34,7 +34,7 @@ interface BodyPortalProps { export const BodyPortal: React.FC> = ({ children, - zIndex = zIndices.floating, + zIndex = zIndexes.floating, }) => { const [ready, setReady] = useState(false); const mode = useCurrentMode(); diff --git a/packages/gamut/src/Box/props.ts b/packages/gamut/src/Box/props.ts index 0d53c0a39a9..c1df00fe3ea 100644 --- a/packages/gamut/src/Box/props.ts +++ b/packages/gamut/src/Box/props.ts @@ -1,4 +1,4 @@ -import { system, zIndices } from '@codecademy/gamut-styles'; +import { system, zIndexes } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import { WithChildrenProp } from '../utils'; @@ -24,7 +24,7 @@ export const sharedStates = system.states({ }, context: { position: 'relative', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, }, 'no-select': { WebkitTouchCallout: 'none', diff --git a/packages/gamut/src/Button/shared/styles.ts b/packages/gamut/src/Button/shared/styles.ts index 534aa43b7de..7a8c2e53cf2 100644 --- a/packages/gamut/src/Button/shared/styles.ts +++ b/packages/gamut/src/Button/shared/styles.ts @@ -4,7 +4,7 @@ import { styledOptions, system, transitionConcat, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { CSSObject, ThemeProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -68,7 +68,7 @@ export const buttonStyles = system.css({ border: 2, inset: -5, opacity: 0, - zIndex: zIndices.base, + zIndex: zIndexes.base, }, [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { opacity: 1, diff --git a/packages/gamut/src/DataList/EmptyRows.tsx b/packages/gamut/src/DataList/EmptyRows.tsx index 811fb5fb99a..42435073631 100644 --- a/packages/gamut/src/DataList/EmptyRows.tsx +++ b/packages/gamut/src/DataList/EmptyRows.tsx @@ -1,4 +1,4 @@ -import { zIndices } from '@codecademy/gamut-styles'; +import { zIndexes } from '@codecademy/gamut-styles'; import { FlexBox } from '../Box'; import { FillButton } from '../Button'; @@ -17,7 +17,7 @@ export const EmptyRows = () => { position="sticky" top="calc(50% - 66px)" width="320px" - zIndex={zIndices.foreground} + zIndex={zIndexes.foreground} > diff --git a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx index 29e2b477461..ca44bd6aa94 100644 --- a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx +++ b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/CalendarWrapper.tsx @@ -1,5 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; -import { zIndices } from '@codecademy/gamut-styles'; +import { zIndexes } from '@codecademy/gamut-styles'; import * as React from 'react'; import { Box } from '../../../Box'; @@ -13,7 +13,7 @@ export const CalendarWrapper: React.FC = ({ children }) => ( border={1} borderRadius="sm" position="relative" - zIndex={zIndices.foreground} + zIndex={zIndexes.foreground} > {children} diff --git a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx index 6a992bb7969..92a965047e2 100644 --- a/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx +++ b/packages/gamut/src/DatePicker/DatePickerCalendar/Calendar/utils/elements.tsx @@ -2,7 +2,7 @@ import { css, states, transitionConcat, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -124,7 +124,7 @@ export const DateCell = styled.td( borderRadius: 'lg', border: 2, opacity: 0, - zIndex: zIndices.base, + zIndex: zIndexes.base, }, '&:focus-visible::before': { opacity: 1, diff --git a/packages/gamut/src/Flyout/index.tsx b/packages/gamut/src/Flyout/index.tsx index adfee0e0c4c..bc365e7cea0 100644 --- a/packages/gamut/src/Flyout/index.tsx +++ b/packages/gamut/src/Flyout/index.tsx @@ -1,5 +1,5 @@ import { MiniDeleteIcon } from '@codecademy/gamut-icons'; -import { Background, Colors, zIndices } from '@codecademy/gamut-styles'; +import { Background, Colors, zIndexes } from '@codecademy/gamut-styles'; import * as React from 'react'; import { FlexBox } from '../Box'; @@ -52,7 +52,7 @@ export const Flyout: React.FC = ({ escapeCloses isOpen={expanded} shroud - zIndex={zIndices.flyout} + zIndex={zIndexes.flyout} onRequestClose={onClose} > diff --git a/packages/gamut/src/Form/SelectDropdown/styles.ts b/packages/gamut/src/Form/SelectDropdown/styles.ts index 281e1e87f8c..0514917c5d7 100644 --- a/packages/gamut/src/Form/SelectDropdown/styles.ts +++ b/packages/gamut/src/Form/SelectDropdown/styles.ts @@ -3,7 +3,7 @@ import { states, theme as GamutTheme, variant, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { StylesConfig } from 'react-select'; @@ -168,7 +168,7 @@ export const getMemoizedStyles = ( ...provided, // The menu is portaled to the body, so it stacks at the page root as a popover — // above sticky headers and modal content. A raw `zIndex` prop overrides as an escape hatch. - zIndex: zIndex ?? zIndices.popover, + zIndex: zIndex ?? zIndexes.popover, }), menuList: (provided, state: BaseSelectComponentProps) => { const sizeInteger = state.selectProps.size === 'small' ? 2 : 3; diff --git a/packages/gamut/src/List/TableHeader.tsx b/packages/gamut/src/List/TableHeader.tsx index 63add861255..5b88e17f20e 100644 --- a/packages/gamut/src/List/TableHeader.tsx +++ b/packages/gamut/src/List/TableHeader.tsx @@ -1,4 +1,4 @@ -import { zIndices } from '@codecademy/gamut-styles'; +import { zIndexes } from '@codecademy/gamut-styles'; import { ComponentProps, forwardRef } from 'react'; import { Box } from '../Box'; @@ -18,7 +18,7 @@ export const TableHeader = forwardRef( display="block" position="sticky" top={0} - zIndex={zIndices.foreground} + zIndex={zIndexes.foreground} > ( flexDirection: { _: 'row', c_base: 'column', c_sm: 'row' }, top: 0, bg: 'background-current', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, fontFamily: 'accent', pb: { _: 0, c_base: 8, c_sm: 0 }, }), @@ -469,7 +469,7 @@ export const StickyHeaderColWrapper = styled.th( height: '100%', top: 0, left: 0, - zIndex: zIndices.underlay, + zIndex: zIndexes.underlay, }, '&:after': { content: '""', @@ -483,7 +483,7 @@ export const StickyHeaderColWrapper = styled.th( height: '100%', top: 0, left: 0, - zIndex: zIndices.underlay, + zIndex: zIndexes.underlay, }, // p: 0 removes the browser's default padding of 1px p: 0, @@ -491,7 +491,7 @@ export const StickyHeaderColWrapper = styled.th( flexShrink: 0, position: 'sticky', left: 0, - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, bg: { _: 'inherit', c_base: 'transparent', c_sm: 'inherit' }, '&:not(:first-of-type)': { left: { _: 16, c_base: 0, c_sm: 16 }, diff --git a/packages/gamut/src/Menu/elements.tsx b/packages/gamut/src/Menu/elements.tsx index 0a36aed2f91..f1f63ca2d23 100644 --- a/packages/gamut/src/Menu/elements.tsx +++ b/packages/gamut/src/Menu/elements.tsx @@ -2,7 +2,7 @@ import { styledOptions, system, transitionConcat, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -89,7 +89,7 @@ const interactiveVariants = system.variant({ alignItems: 'center', cursor: 'pointer', width: 1, - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, px: 24, py: 12, position: 'relative', @@ -109,7 +109,7 @@ const interactiveVariants = system.variant({ border: 2, borderColor: 'primary', opacity: 0, - zIndex: zIndices.underlay, + zIndex: zIndexes.underlay, }, [MenuItemSelectors.OUTLINE_FOCUS_VISIBLE]: { opacity: 1, @@ -225,7 +225,7 @@ const StyledListLink = styled('a', styledOptions<'a'>())( export const ListLink = forwardRef< HTMLAnchorElement, ComponentProps ->(({ zIndex = zIndices.foreground, ...rest }, ref) => ( +>(({ zIndex = zIndexes.foreground, ...rest }, ref) => ( )); diff --git a/packages/gamut/src/Overlay/index.tsx b/packages/gamut/src/Overlay/index.tsx index e4512961341..3df3039d44e 100644 --- a/packages/gamut/src/Overlay/index.tsx +++ b/packages/gamut/src/Overlay/index.tsx @@ -1,4 +1,4 @@ -import { states, zIndices } from '@codecademy/gamut-styles'; +import { states, zIndexes } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useCallback } from 'react'; import * as React from 'react'; @@ -34,9 +34,9 @@ export type OverlayProps = { /** Whether the overlay allows scroll */ allowScroll?: boolean; /** - * Stacking layer for the Overlay. Pass a `zIndices` token or a raw number (escape - * hatch). Defaults to `zIndices.modal`; a portaled side panel should pass - * `zIndices.flyout` to sit below modals. + * Stacking layer for the Overlay. Pass a `zIndexes` token or a raw number (escape + * hatch). Defaults to `zIndexes.modal`; a portaled side panel should pass + * `zIndexes.flyout` to sit below modals. */ zIndex?: number; }; @@ -62,7 +62,7 @@ export const Overlay: React.FC = ({ onRequestClose, isOpen, allowScroll = false, - zIndex = zIndices.modal, + zIndex = zIndexes.modal, }) => { const handleOutsideClick = useCallback(() => { if (clickOutsideCloses) { diff --git a/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx b/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx index 3bf50b9e0be..701d81a61d5 100644 --- a/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx +++ b/packages/gamut/src/PatternBackdrop/PatternBackdrop.tsx @@ -1,5 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; -import { styledOptions, system, zIndices } from '@codecademy/gamut-styles'; +import { styledOptions, system, zIndexes } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; import { ComponentProps, forwardRef } from 'react'; @@ -13,7 +13,7 @@ const PatternBackdropBody = styled('div', styledOptions)< >( system.css({ position: 'relative', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, bg: 'background', border: 1, maxWidth: 1, @@ -33,7 +33,7 @@ export const PatternBackdrop = forwardRef( display="inline-block" maxWidth="100%" position="relative" - zIndex={zIndices.foreground} + zIndex={zIndexes.foreground} > {isOpen && ( - + ) : ( - + ); export type PopoverContainerProps = Pick; diff --git a/packages/gamut/src/Popover/styles/base.ts b/packages/gamut/src/Popover/styles/base.ts index 59fcf3c5545..b05bc33dcd9 100644 --- a/packages/gamut/src/Popover/styles/base.ts +++ b/packages/gamut/src/Popover/styles/base.ts @@ -1,4 +1,4 @@ -import { states, variant, zIndices } from '@codecademy/gamut-styles'; +import { states, variant, zIndexes } from '@codecademy/gamut-styles'; import { toolTipBodyCss } from '../../Tip/shared/styles/styles'; @@ -31,7 +31,7 @@ export const popoverStates = states({ export const raisedDivVariants = variant({ base: { - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, }, defaultVariant: 'primary', variants: { diff --git a/packages/gamut/src/Popover/styles/variants.ts b/packages/gamut/src/Popover/styles/variants.ts index ba22712decf..c3d37b3c2d1 100644 --- a/packages/gamut/src/Popover/styles/variants.ts +++ b/packages/gamut/src/Popover/styles/variants.ts @@ -1,4 +1,4 @@ -import { states, variant, zIndices } from '@codecademy/gamut-styles'; +import { states, variant, zIndexes } from '@codecademy/gamut-styles'; import { createVariantsFromAlignments } from '../../Tip/shared/styles/createVariantsUtils'; import { tooltipArrowHeight } from '../../Tip/shared/styles/styles'; @@ -47,7 +47,7 @@ const beakVariantStyles = createVariantsFromAlignments( export const beakVariants = variant({ base: { background: 'transparent', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, position: 'fixed', }, prop: 'beak', diff --git a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx index 0259a1e9272..158bb43687f 100644 --- a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx +++ b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx @@ -2,7 +2,7 @@ import { elementDir, system, useElementDir, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -287,5 +287,5 @@ export const PopoverContainer: React.FC = ({ if (inline) return content; - return {content}; + return {content}; }; diff --git a/packages/gamut/src/Tabs/TabButton.tsx b/packages/gamut/src/Tabs/TabButton.tsx index fc150c6560a..8616642b8d2 100644 --- a/packages/gamut/src/Tabs/TabButton.tsx +++ b/packages/gamut/src/Tabs/TabButton.tsx @@ -1,4 +1,4 @@ -import { states, variant, zIndices } from '@codecademy/gamut-styles'; +import { states, variant, zIndexes } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -29,7 +29,7 @@ const focusVisibleStyles = { borderColor: 'primary', position: 'absolute', inset: 0, - zIndex: zIndices.base, + zIndex: zIndexes.base, borderRadiusTop: 'md', borderRadiusBottom: 'none', }, @@ -48,7 +48,7 @@ export const tabVariants = variant({ color: 'text', font: 'inherit', cursor: 'pointer', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, [TabSelectors.DISABLED]: { opacity: 0.25, cursor: 'not-allowed', diff --git a/packages/gamut/src/Tabs/Tabs.tsx b/packages/gamut/src/Tabs/Tabs.tsx index 13332160986..54247d87aea 100644 --- a/packages/gamut/src/Tabs/Tabs.tsx +++ b/packages/gamut/src/Tabs/Tabs.tsx @@ -1,4 +1,4 @@ -import { Background, zIndices } from '@codecademy/gamut-styles'; +import { Background, zIndexes } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; import * as React from 'react'; @@ -25,10 +25,10 @@ export const Tabs: React.FC = (props) => { {/* currently only supporting dark mode for the LE variant. */} {props.variant === 'block' ? ( - + ) : ( - + )} ); diff --git a/packages/gamut/src/Tabs/styles.tsx b/packages/gamut/src/Tabs/styles.tsx index e445e6c8686..f5bc116fabb 100644 --- a/packages/gamut/src/Tabs/styles.tsx +++ b/packages/gamut/src/Tabs/styles.tsx @@ -1,4 +1,4 @@ -import { system, variant, zIndices } from '@codecademy/gamut-styles'; +import { system, variant, zIndexes } from '@codecademy/gamut-styles'; export const tabContainerVariants = variant({ base: { @@ -15,7 +15,7 @@ export const tabContainerVariants = variant({ bg: 'text', position: 'absolute', bottom: 0, - zIndex: zIndices.base, + zIndex: zIndexes.base, width: '100%', }, }, diff --git a/packages/gamut/src/Tip/PreviewTip/elements.tsx b/packages/gamut/src/Tip/PreviewTip/elements.tsx index 21cde41ef91..c200b485e8e 100644 --- a/packages/gamut/src/Tip/PreviewTip/elements.tsx +++ b/packages/gamut/src/Tip/PreviewTip/elements.tsx @@ -1,5 +1,5 @@ import { CheckerDense } from '@codecademy/gamut-patterns'; -import { css, variant, zIndices } from '@codecademy/gamut-styles'; +import { css, variant, zIndexes } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import { useMemo } from 'react'; @@ -147,7 +147,7 @@ export const PreviewTipShadow: React.FC = ({ return ( diff --git a/packages/gamut/src/Tip/shared/InlineTip.tsx b/packages/gamut/src/Tip/shared/InlineTip.tsx index b59608e1459..a1141455f52 100644 --- a/packages/gamut/src/Tip/shared/InlineTip.tsx +++ b/packages/gamut/src/Tip/shared/InlineTip.tsx @@ -1,4 +1,4 @@ -import { zIndices } from '@codecademy/gamut-styles'; +import { zIndexes } from '@codecademy/gamut-styles'; import { InfoTipContainer } from '../InfoTip/styles'; import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements'; @@ -57,7 +57,7 @@ export const InlineTip: React.FC = ({ const tipBody = ( = ({ colorMode = 'light', }) => { return ( - + diff --git a/packages/gamut/src/Typography/Text.tsx b/packages/gamut/src/Typography/Text.tsx index a05bf9d53d0..5c4bd43818c 100644 --- a/packages/gamut/src/Typography/Text.tsx +++ b/packages/gamut/src/Typography/Text.tsx @@ -3,7 +3,7 @@ import { styledOptions, system, variant, - zIndices, + zIndexes, } from '@codecademy/gamut-styles'; import { StyleProps, variance } from '@codecademy/variance'; import styled from '@emotion/styled'; @@ -77,7 +77,7 @@ const textStates = states({ fontWeight: 'bold', minWidth: '0.4rem', position: 'relative', - zIndex: zIndices.foreground, + zIndex: zIndexes.foreground, // the text is more legible against the background color with text smoothing MozOsxFontSmoothing: 'grayscale', WebkitFontSmoothing: 'antialiased', @@ -91,7 +91,7 @@ const textStates = states({ position: 'absolute', top: '50%', width: 'calc(100% + 0.4rem)', - zIndex: zIndices.underlay, + zIndex: zIndexes.underlay, }, }, screenreader: { diff --git a/packages/styleguide/src/lib/Foundations/ZIndex.mdx b/packages/styleguide/src/lib/Foundations/ZIndex.mdx index 308d78538be..5f8648b9125 100644 --- a/packages/styleguide/src/lib/Foundations/ZIndex.mdx +++ b/packages/styleguide/src/lib/Foundations/ZIndex.mdx @@ -17,26 +17,26 @@ export const parameters = { z-index should only be used when necessary to override the default stacking order. When possible, use the default stacking order and avoid using z-index. -To standardize z-index values across the application, Gamut exposes one semantic z-index scale, `zIndices`, so stacking order is coordinated by name +To standardize z-index values across the application, Gamut exposes one semantic z-index scale, `zIndexes`, so stacking order is coordinated by name instead of by scattered magic numbers. Import it from `@codecademy/gamut-styles` and pass a token to the `zIndex` prop: ```tsx -import { zIndices } from '@codecademy/gamut-styles'; +import { zIndexes } from '@codecademy/gamut-styles'; -; +; ``` ## The scale - + ## Custom z-index values -The `zIndex` prop is left **numeric**. `zIndices` is just a numeric object you reference by name; the prop still accepts a raw number. This is deliberate in situations where you need to set a z-index that is not part of the scale. +The `zIndex` prop is left **numeric**. `zIndexes` is just a numeric object you reference by name; the prop still accepts a raw number. This is deliberate in situations where you need to set a z-index that is not part of the scale. ```tsx -; // just above modal +; // just above modal ; // deliberate one-off ``` @@ -44,7 +44,7 @@ These situations should be rare, but when they do arise, you can use the scale a ## Guidance -- **Reach for a token first.** `zIndices.modal`, `zIndices.foreground`, etc. — the name documents +- **Reach for a token first.** `zIndexes.modal`, `zIndexes.foreground`, etc. — the name documents intent far better than a number. - **Escape hatch sparingly.** A raw number is fine for a genuine one-off, but prefer `token ± n` so the relationship to the scale stays visible, and leave a comment. diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index 1bcd9f8e998..a77a5957cb2 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -6,7 +6,7 @@ import { lxStudioColors, theme, trueColors, - zIndices as zIndicesTokens, + zIndexes as zIndexesTokens, } from '@codecademy/gamut-styles'; // eslint-disable-next-line gamut/import-paths import * as ALL_PROPS from '@codecademy/gamut-styles/src/variance/config'; @@ -476,9 +476,9 @@ const zIndexNotes: Record = { appBar: 'App header / nav (app-owned)', }; -export const zIndices = { +export const zIndexes = { // Object insertion order runs low → high (underlay … tooltip). - rows: Object.entries(zIndicesTokens).map(([id, value]) => ({ + rows: Object.entries(zIndexesTokens).map(([id, value]) => ({ id, value, })), @@ -486,7 +486,7 @@ export const zIndices = { { ...PROP_COLUMN, name: 'Token' }, { ...PATH_COLUMN, - render: ({ id }: any) => zIndices.{id}, + render: ({ id }: any) => zIndexes.{id}, }, VALUE_COLUMN, { From 0c81fd4ee9c7f2ff8603da379e4cbe9bb3237665 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 13 Jul 2026 10:53:32 -0400 Subject: [PATCH 10/10] removed unused import --- packages/styleguide/src/lib/Foundations/ZIndex.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/styleguide/src/lib/Foundations/ZIndex.mdx b/packages/styleguide/src/lib/Foundations/ZIndex.mdx index 5f8648b9125..03e1911c96f 100644 --- a/packages/styleguide/src/lib/Foundations/ZIndex.mdx +++ b/packages/styleguide/src/lib/Foundations/ZIndex.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/addon-docs/blocks'; -import { ComponentHeader, LinkTo, TokenTable } from '~styleguide/blocks'; +import { ComponentHeader, TokenTable } from '~styleguide/blocks'; import * as TABLES from './shared/elements';