Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (`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.
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 { 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 };`,
`<Box zIndex={zIndices.stickyHeader} />;`,
// Arithmetic on a token is allowed (e.g. Tip's shadow).
`const styles = { zIndex: zIndices.foreground - 2 };`,
`<Box zIndex={zIndices.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' }],
},
],
});
62 changes: 62 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,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: `<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 `zIndices` 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.',
},
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