Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
26fa227
added z-index soft scale
LinKCoding Jul 8, 2026
c01bd80
update eslint messages and desc
LinKCoding Jul 8, 2026
96635b0
updates to zIndex scale
LinKCoding Jul 8, 2026
4d0c25c
added a story
LinKCoding Jul 8, 2026
32cfd25
linting errors
LinKCoding Jul 8, 2026
83c108a
updated snapshots
LinKCoding Jul 8, 2026
715f4f0
add version plan
LinKCoding Jul 8, 2026
2b82038
update docs to reflect changes
LinKCoding Jul 10, 2026
a7d0445
updated naming to zIndexes
LinKCoding Jul 13, 2026
0c81fd4
removed unused import
LinKCoding Jul 13, 2026
1bf763f
updated snapshot
LinKCoding Jul 14, 2026
1899413
update snapshot
LinKCoding Jul 14, 2026
8621167
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 14, 2026
774cc3a
add in checkoutv4 for workflow
LinKCoding Jul 14, 2026
2b51485
remove recommended field in eslint
LinKCoding Jul 14, 2026
70f6985
remove recommended field in eslint
LinKCoding Jul 14, 2026
6faff66
update ruletester
LinKCoding Jul 14, 2026
8fe82d3
update parser option too
LinKCoding Jul 14, 2026
bcd1932
undo recommended removal
LinKCoding Jul 15, 2026
7b186c2
add test for selectdropdown menuportal
LinKCoding Jul 15, 2026
282d113
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 15, 2026
79b474e
update eslint mdx story
LinKCoding Jul 15, 2026
74b7a98
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 15, 2026
9743432
more linting fixes
LinKCoding Jul 16, 2026
b6c600e
updating selectdropdown default zindex
LinKCoding Jul 17, 2026
8c31500
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 17, 2026
310a8ec
update zindex type for selectdropdown
LinKCoding Jul 17, 2026
f72608b
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 17, 2026
a1d426f
add test for bodyportal
LinKCoding Jul 17, 2026
980e9fd
Merge branch 'kl-gmt-308-zindex' into kl-gmt-308-zindex-beta
LinKCoding Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish-alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/prerelease-publish
with:
node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }}
Expand Down
34 changes: 34 additions & 0 deletions .nx/version-plans/version-plan-1783541829799.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
eslint-plugin-gamut: major
gamut-styles: minor
gamut: major
gamut-kit: major
---

Add a semantic z-index scale (`zIndexes`) and migrate Gamut off ad-hoc z-index values.

**gamut-styles**

- 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={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**

- ⚠️ 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 `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 `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.
2 changes: 2 additions & 0 deletions packages/eslint-plugin-gamut/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
};

Expand Down
55 changes: 55 additions & 0 deletions packages/eslint-plugin-gamut/src/no-raw-z-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { TSESLint } from '@typescript-eslint/utils';

import rule from './no-raw-z-index';

const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
});

ruleTester.run('no-raw-z-index', rule, {
valid: [
// Semantic tokens are the expected usage.
`const styles = { zIndex: zIndexes.modal };`,
`<Box zIndex={zIndexes.stickyHeader} />;`,
// Arithmetic on a token is allowed (e.g. Tip's shadow).
`const styles = { zIndex: zIndexes.foreground - 2 };`,
`<Box zIndex={zIndexes.modal + 5} />;`,
// Variables / non-literal expressions are not flagged.
`<Box zIndex={zIndex} />;`,
`const styles = { zIndex };`,
// Unrelated properties.
`const styles = { padding: 0 };`,
`<Box top={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: `<Box zIndex={2} />;`,
errors: [{ messageId: 'noRawZIndex' }],
},
{
code: `<Box zIndex={-1} />;`,
errors: [{ messageId: 'noRawZIndex' }],
},
],
});
61 changes: 61 additions & 0 deletions packages/eslint-plugin-gamut/src/no-raw-z-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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: `<Box zIndex={1} />`
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:
'Discourage raw numeric z-index values which can lead to z-index stacking issues and encourage usage of semantic tokens from the `zIndexes` scale.',
},
messages: {
noRawZIndex:
'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: [],
},
name: 'no-raw-z-index',
});
1 change: 1 addition & 0 deletions packages/eslint-plugin-gamut/src/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
Loading
Loading