@hirobius/design-system 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +123 -0
- package/CONSUMING.md +11 -6
- package/DESIGN.md +34 -21
- package/NOTICE.md +1 -1
- package/README.md +48 -13
- package/codemods/hds-prefix.mjs +231 -0
- package/codemods/jsx-fold.mjs +515 -0
- package/codemods/not-found-pattern.mjs +71 -0
- package/codemods/patterns-subpath.mjs +242 -81
- package/codemods/patterns-subpath.names.json +41 -15
- package/codemods/removed-0.20.json +194 -0
- package/codemods/tile-grid.mjs +283 -0
- package/codemods/unrewritable.mjs +291 -0
- package/dist/chunks/DemoStateContext-Cz5TmrYu.js +32 -0
- package/dist/chunks/empty-state-DNoThgfV.js +1227 -0
- package/dist/chunks/form-By82qZcC.js +66 -0
- package/dist/contexts.js +157 -78
- package/dist/fonts/geist-mono-400.woff2 +0 -0
- package/dist/fonts/satoshi-400.woff2 +0 -0
- package/dist/fonts/satoshi-500.woff2 +0 -0
- package/dist/fonts/satoshi-700.woff2 +0 -0
- package/dist/fonts.css +53 -0
- package/dist/form.js +1 -1
- package/dist/hirobius-ui.js +1600 -5354
- package/dist/manifest.js +12 -13
- package/dist/patterns.js +796 -510
- package/dist/styles.css +2 -2
- package/dist/tokens.css +1 -1
- package/dist/tokens.js +165 -65
- package/dist/types/src/app/components/alert-dialog.d.ts +5 -0
- package/dist/types/src/app/components/asset-img.d.ts +1 -0
- package/dist/types/src/app/components/avatar-group.d.ts +2 -2
- package/dist/types/src/app/components/badge.d.ts +8 -3
- package/dist/types/src/app/components/blockquote.d.ts +2 -2
- package/dist/types/src/app/components/box-sx.d.ts +110 -13
- package/dist/types/src/app/components/box.d.ts +6 -3
- package/dist/types/src/app/components/breadcrumb.d.ts +0 -5
- package/dist/types/src/app/components/button.d.ts +35 -4
- package/dist/types/src/app/components/card.d.ts +10 -10
- package/dist/types/src/app/components/combobox.d.ts +47 -11
- package/dist/types/src/app/components/container.d.ts +2 -2
- package/dist/types/src/app/components/data-table-section.d.ts +2 -1
- package/dist/types/src/app/components/destructive-section.d.ts +1 -0
- package/dist/types/src/app/components/dialog.d.ts +8 -1
- package/dist/types/src/app/components/disclosure.d.ts +1 -5
- package/dist/types/src/app/components/error-pattern.d.ts +6 -1
- package/dist/types/src/app/components/field.d.ts +1 -2
- package/dist/types/src/app/components/form-actions.d.ts +1 -0
- package/dist/types/src/app/components/form-control.d.ts +10 -0
- package/dist/types/src/app/components/grid.d.ts +15 -6
- package/dist/types/src/app/components/hds-tooltip.d.ts +12 -2
- package/dist/types/src/app/components/icon.d.ts +1 -0
- package/dist/types/src/app/components/inline-code.d.ts +1 -2
- package/dist/types/src/app/components/input.d.ts +8 -5
- package/dist/types/src/app/components/kbd.d.ts +2 -2
- package/dist/types/src/app/components/metadata-list.d.ts +2 -2
- package/dist/types/src/app/components/metric-tiles.d.ts +1 -0
- package/dist/types/src/app/components/page-header.d.ts +0 -2
- package/dist/types/src/app/components/popover.d.ts +8 -0
- package/dist/types/src/app/components/progress.d.ts +33 -6
- package/dist/types/src/app/components/segmented-control.d.ts +2 -26
- package/dist/types/src/app/components/select.d.ts +10 -1
- package/dist/types/src/app/components/skeleton.d.ts +3 -4
- package/dist/types/src/app/components/spinner.d.ts +1 -2
- package/dist/types/src/app/components/stack.d.ts +4 -1
- package/dist/types/src/app/components/stat.d.ts +5 -5
- package/dist/types/src/app/components/status-list-item.d.ts +1 -2
- package/dist/types/src/app/components/status-tile.d.ts +3 -2
- package/dist/types/src/app/components/surface.d.ts +2 -3
- package/dist/types/src/app/components/switcher.d.ts +1 -1
- package/dist/types/src/app/components/table.d.ts +6 -15
- package/dist/types/src/app/components/tag.d.ts +0 -6
- package/dist/types/src/app/components/text.d.ts +54 -8
- package/dist/types/src/app/components/textarea.d.ts +1 -2
- package/dist/types/src/app/components/timestamp.d.ts +1 -0
- package/dist/types/src/app/components/toast.d.ts +1 -2
- package/dist/types/src/app/components/tooltip.d.ts +1 -1
- package/dist/types/src/app/context/DemoStateContext.d.ts +1 -1
- package/dist/types/src/app/context/RouterContext.d.ts +5 -5
- package/dist/types/src/app/data/manifest-types.d.ts +10 -5
- package/dist/types/src/app/design-system/generated-token-refs.d.ts +45 -28
- package/dist/types/src/app/design-system/generated-token-values.d.ts +4 -0
- package/dist/types/src/app/design-system/tokens.d.ts +139 -18
- package/dist/types/src/icons.d.ts +1 -1
- package/dist/types/src/index.d.ts +76 -164
- package/dist/types/src/patterns.d.ts +20 -25
- package/dist/variables.css +47 -33
- package/docs/CONSUMING.md +80 -48
- package/hirobius.tokens.json +125 -88
- package/llms.txt +166 -59
- package/mcp/catalog.mjs +366 -0
- package/mcp/guide.mjs +315 -0
- package/mcp/hds-mcp.mjs +49 -0
- package/mcp/server.mjs +94 -0
- package/package.json +48 -27
- package/public/hds-manifest.json +2667 -6705
- package/public/llms/components.txt +45 -426
- package/public/llms/layout.txt +5 -5
- package/public/llms-full.txt +245 -506
- package/public/llms.txt +166 -59
- package/scripts/eslint-plugin-hds/index.d.mts +29 -0
- package/scripts/eslint-plugin-hds/index.mjs +55 -0
- package/scripts/eslint-plugin-hds/package.json +30 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-controls.mjs +47 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-hex.mjs +70 -0
- package/scripts/eslint-plugin-hds/rules/no-raw-px-spacing.mjs +125 -0
- package/scripts/eslint-plugin-hds/rules/prefer-hds-layout-primitive.mjs +54 -0
- package/scripts/eslint-plugin-hds/rules/sx-token-first.mjs +111 -0
- package/scripts/eslint-plugin-hds/rules/utils.mjs +66 -0
- package/src/app/data/component-api.json +1082 -2913
- package/dist/chunks/LanguageContext-BxdCwiFg.js +0 -95
- package/dist/chunks/TokenDisplayContext-DKYJMVWk.js +0 -26
- package/dist/chunks/activity-feed-BGQ6v3wc.js +0 -22040
- package/dist/chunks/form-COP3vK9_.js +0 -66
- package/dist/types/src/app/components/activity-feed.d.ts +0 -73
- package/dist/types/src/app/components/app-shell.d.ts +0 -40
- package/dist/types/src/app/components/aspect-ratio.d.ts +0 -25
- package/dist/types/src/app/components/bleed.d.ts +0 -39
- package/dist/types/src/app/components/button-group.d.ts +0 -22
- package/dist/types/src/app/components/calendar.d.ts +0 -22
- package/dist/types/src/app/components/carousel.d.ts +0 -26
- package/dist/types/src/app/components/center.d.ts +0 -36
- package/dist/types/src/app/components/cinematic-link.d.ts +0 -18
- package/dist/types/src/app/components/circular-progress.d.ts +0 -28
- package/dist/types/src/app/components/cluster.d.ts +0 -39
- package/dist/types/src/app/components/command-palette.d.ts +0 -46
- package/dist/types/src/app/components/component-instance-matrix.d.ts +0 -27
- package/dist/types/src/app/components/context-menu.d.ts +0 -78
- package/dist/types/src/app/components/cover.d.ts +0 -43
- package/dist/types/src/app/components/date-input.d.ts +0 -31
- package/dist/types/src/app/components/date-range-input.d.ts +0 -34
- package/dist/types/src/app/components/date-time-input.d.ts +0 -30
- package/dist/types/src/app/components/doc-link-card.d.ts +0 -32
- package/dist/types/src/app/components/error-boundary.d.ts +0 -25
- package/dist/types/src/app/components/file-input.d.ts +0 -30
- package/dist/types/src/app/components/foundation-swatch.d.ts +0 -45
- package/dist/types/src/app/components/frame.d.ts +0 -38
- package/dist/types/src/app/components/heading-stack.d.ts +0 -52
- package/dist/types/src/app/components/history-card.d.ts +0 -12
- package/dist/types/src/app/components/hover-card.d.ts +0 -46
- package/dist/types/src/app/components/icon-button.d.ts +0 -22
- package/dist/types/src/app/components/image-lightbox.d.ts +0 -14
- package/dist/types/src/app/components/input-group.d.ts +0 -23
- package/dist/types/src/app/components/multi-selector.d.ts +0 -40
- package/dist/types/src/app/components/nav-group.d.ts +0 -29
- package/dist/types/src/app/components/nav-item.d.ts +0 -33
- package/dist/types/src/app/components/not-found-pattern.d.ts +0 -1
- package/dist/types/src/app/components/overflow-list.d.ts +0 -21
- package/dist/types/src/app/components/selectable-card.d.ts +0 -29
- package/dist/types/src/app/components/side-nav.d.ts +0 -23
- package/dist/types/src/app/components/sketch.d.ts +0 -19
- package/dist/types/src/app/components/stacked-card-rail.d.ts +0 -32
- package/dist/types/src/app/components/status-dot.d.ts +0 -25
- package/dist/types/src/app/components/stepper-field.d.ts +0 -12
- package/dist/types/src/app/components/stepper.d.ts +0 -33
- package/dist/types/src/app/components/text-lockup.d.ts +0 -38
- package/dist/types/src/app/components/tile-grid.d.ts +0 -17
- package/dist/types/src/app/components/time-input.d.ts +0 -24
- package/dist/types/src/app/components/toggle-button.d.ts +0 -32
- package/dist/types/src/app/components/token.d.ts +0 -90
- package/dist/types/src/app/components/tokenUtils.d.ts +0 -38
- package/dist/types/src/app/components/tokenizer.d.ts +0 -24
- package/dist/types/src/app/components/toolbar.d.ts +0 -36
- package/dist/types/src/app/components/top-nav.d.ts +0 -23
- package/dist/types/src/app/components/tree-list.d.ts +0 -30
- package/dist/types/src/app/data/nav-model.d.ts +0 -39
- package/dist/types/src/app/hooks/useFocusVisible.d.ts +0 -9
- package/dist/types/src/app/layouts/CaseStudyLayout.d.ts +0 -20
- package/dist/types/src/app/layouts/DocLayout.d.ts +0 -14
- package/dist/types/src/app/layouts/EmbeddedDocLayoutContext.d.ts +0 -6
- package/dist/types/src/app/layouts/HdsDocsShell.d.ts +0 -72
- package/dist/types/src/app/layouts/HdsSystemDocLayout.d.ts +0 -14
- package/dist/types/src/app/lib/hds-search.d.ts +0 -45
- package/dist/types/src/app/lib/nav-active.d.ts +0 -16
- package/dist/types/src/app/lib/navLevels.d.ts +0 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hds/no-raw-px-spacing
|
|
3
|
+
*
|
|
4
|
+
* Disallows raw pixel strings (`'16px'`) or bare numeric literals (`16`) on
|
|
5
|
+
* margin/padding/gap family properties inside a JSX `style={{ ... }}`
|
|
6
|
+
* object. Zero is exempt (a reset has no scale to violate). Mirrors the
|
|
7
|
+
* intent of scripts/check-hardcoded-spacing.mjs for consumer app code.
|
|
8
|
+
*
|
|
9
|
+
* The fix it offers is the t-shirt scale token, and, where Box `sx` has a
|
|
10
|
+
* key for the same CSS property, that key with a step name. Box `sx` resolves
|
|
11
|
+
* step names on its spacing keys only (`mb: 'md'`); a long-hand key
|
|
12
|
+
* (`marginBottom: 'md'`) passes through as invalid CSS and renders no
|
|
13
|
+
* spacing, so the message names the shorthand, never the flagged prop.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
attrName,
|
|
17
|
+
jsxObjectExpression,
|
|
18
|
+
numberLiteralValue,
|
|
19
|
+
propName,
|
|
20
|
+
stringLiteralValue,
|
|
21
|
+
} from './utils.mjs';
|
|
22
|
+
|
|
23
|
+
const SPACING_PROPS = new Set([
|
|
24
|
+
'margin',
|
|
25
|
+
'marginTop',
|
|
26
|
+
'marginBottom',
|
|
27
|
+
'marginLeft',
|
|
28
|
+
'marginRight',
|
|
29
|
+
'marginBlock',
|
|
30
|
+
'marginInline',
|
|
31
|
+
'marginBlockStart',
|
|
32
|
+
'marginBlockEnd',
|
|
33
|
+
'marginInlineStart',
|
|
34
|
+
'marginInlineEnd',
|
|
35
|
+
'padding',
|
|
36
|
+
'paddingTop',
|
|
37
|
+
'paddingBottom',
|
|
38
|
+
'paddingLeft',
|
|
39
|
+
'paddingRight',
|
|
40
|
+
'paddingBlock',
|
|
41
|
+
'paddingInline',
|
|
42
|
+
'paddingBlockStart',
|
|
43
|
+
'paddingBlockEnd',
|
|
44
|
+
'paddingInlineStart',
|
|
45
|
+
'paddingInlineEnd',
|
|
46
|
+
'gap',
|
|
47
|
+
'rowGap',
|
|
48
|
+
'columnGap',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Long-hand style prop → the Box sx key that sets the same CSS property and
|
|
52
|
+
// resolves t-shirt steps (src/app/components/box-sx.ts SPACING_PROP_MAP).
|
|
53
|
+
// Logical props have none: sx `mx`/`my`/`px`/`py` set physical sides, which
|
|
54
|
+
// match `*Inline`/`*Block` only in horizontal writing modes.
|
|
55
|
+
const SX_SHORTHAND = {
|
|
56
|
+
margin: 'm',
|
|
57
|
+
marginTop: 'mt',
|
|
58
|
+
marginBottom: 'mb',
|
|
59
|
+
marginLeft: 'ml',
|
|
60
|
+
marginRight: 'mr',
|
|
61
|
+
padding: 'p',
|
|
62
|
+
paddingTop: 'pt',
|
|
63
|
+
paddingBottom: 'pb',
|
|
64
|
+
paddingLeft: 'pl',
|
|
65
|
+
paddingRight: 'pr',
|
|
66
|
+
gap: 'gap',
|
|
67
|
+
rowGap: 'rowGap',
|
|
68
|
+
columnGap: 'columnGap',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const PX_STRING_RE = /^-?\d+(?:\.\d+)?px$/;
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
meta: {
|
|
75
|
+
type: 'problem',
|
|
76
|
+
docs: {
|
|
77
|
+
description:
|
|
78
|
+
'Disallow raw px strings or bare numbers on margin/padding/gap in inline style — use an HDS spacing token.',
|
|
79
|
+
recommended: true,
|
|
80
|
+
url: 'https://github.com/hirobius/hds/blob/main/scripts/eslint-plugin-hds/README.md#hdsno-raw-px-spacing',
|
|
81
|
+
},
|
|
82
|
+
schema: [],
|
|
83
|
+
messages: {
|
|
84
|
+
rawPxSpacing:
|
|
85
|
+
'Raw spacing value "{{value}}" on "{{prop}}" bypasses the HDS spacing scale. Use hds.semantic.space.scale.* (var(--semantic-space-scale-*)), or Box sx prop "{{sxProp}}" with a t-shirt step such as "md".',
|
|
86
|
+
rawPxSpacingNoSxShorthand:
|
|
87
|
+
'Raw spacing value "{{value}}" on "{{prop}}" bypasses the HDS spacing scale. Use hds.semantic.space.scale.* (var(--semantic-space-scale-*)). Box sx has no shorthand for "{{prop}}", so its t-shirt steps do not apply.',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
create(context) {
|
|
91
|
+
function report(node, prop, value) {
|
|
92
|
+
const sxProp = Object.hasOwn(SX_SHORTHAND, prop) ? SX_SHORTHAND[prop] : undefined;
|
|
93
|
+
context.report(
|
|
94
|
+
sxProp
|
|
95
|
+
? { node, messageId: 'rawPxSpacing', data: { prop, value, sxProp } }
|
|
96
|
+
: { node, messageId: 'rawPxSpacingNoSxShorthand', data: { prop, value } },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
JSXAttribute(node) {
|
|
102
|
+
if (attrName(node) !== 'style') return;
|
|
103
|
+
const obj = jsxObjectExpression(node.value);
|
|
104
|
+
if (!obj) return;
|
|
105
|
+
|
|
106
|
+
for (const prop of obj.properties) {
|
|
107
|
+
if (prop.type !== 'Property') continue;
|
|
108
|
+
const name = propName(prop.key);
|
|
109
|
+
if (!name || !SPACING_PROPS.has(name)) continue;
|
|
110
|
+
|
|
111
|
+
const strVal = stringLiteralValue(prop.value);
|
|
112
|
+
if (strVal != null && PX_STRING_RE.test(strVal)) {
|
|
113
|
+
report(prop.value, name, strVal);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const numVal = numberLiteralValue(prop.value);
|
|
118
|
+
if (numVal != null && numVal !== 0) {
|
|
119
|
+
report(prop.value, name, String(numVal));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hds/prefer-hds-layout-primitive
|
|
3
|
+
*
|
|
4
|
+
* Warns (never errors) on ad-hoc `display: 'flex' | 'grid'` inside an
|
|
5
|
+
* inline `style={{ ... }}` object, where a named HDS layout primitive
|
|
6
|
+
* (Stack for flex, Grid for grid — see src/app/components/box.tsx's
|
|
7
|
+
* "reach for Stack/Grid/Container first" guidance) likely fits better than
|
|
8
|
+
* a one-off flex/grid container. Intentionally a warning: ad-hoc flex is
|
|
9
|
+
* sometimes the right call (e.g. deeply nested one-off alignment), so this
|
|
10
|
+
* nudges rather than blocks.
|
|
11
|
+
*/
|
|
12
|
+
import { attrName, jsxObjectExpression, propName, stringLiteralValue } from './utils.mjs';
|
|
13
|
+
|
|
14
|
+
const FLEX_GRID_RE = /^(?:inline-)?(flex|grid)$/;
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
'Warn on ad-hoc display:flex/grid in inline style where a named HDS layout primitive (Stack/Grid) likely fits.',
|
|
22
|
+
recommended: true,
|
|
23
|
+
url: 'https://github.com/hirobius/hds/blob/main/scripts/eslint-plugin-hds/README.md#hdsprefer-hds-layout-primitive',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
preferPrimitive:
|
|
28
|
+
'Ad-hoc "display: {{display}}" in inline style — consider the named HDS layout primitive (Stack for flex, Grid for grid) instead of a one-off flex/grid container.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
JSXAttribute(node) {
|
|
34
|
+
if (attrName(node) !== 'style') return;
|
|
35
|
+
const obj = jsxObjectExpression(node.value);
|
|
36
|
+
if (!obj) return;
|
|
37
|
+
|
|
38
|
+
const displayProp = obj.properties.find(
|
|
39
|
+
(p) => p.type === 'Property' && propName(p.key) === 'display',
|
|
40
|
+
);
|
|
41
|
+
if (!displayProp) return;
|
|
42
|
+
|
|
43
|
+
const value = stringLiteralValue(displayProp.value);
|
|
44
|
+
if (value != null && FLEX_GRID_RE.test(value)) {
|
|
45
|
+
context.report({
|
|
46
|
+
node: displayProp.value,
|
|
47
|
+
messageId: 'preferPrimitive',
|
|
48
|
+
data: { display: value },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hds/sx-token-first
|
|
3
|
+
*
|
|
4
|
+
* Errors on raw hex colors or raw px strings inside a `sx={{ ... }}` prop
|
|
5
|
+
* (Box's token-first layout engine — src/app/components/box.tsx). `sx` is
|
|
6
|
+
* the sanctioned escape hatch specifically because it forces spacing/color
|
|
7
|
+
* through HDS tokens. The spacing shorthands (`p`, `m`, `gap`, ...) take the
|
|
8
|
+
* t-shirt scale by name ('xs' | 'sm' | 'md' | 'lg' | 'xl', hds#206), and the
|
|
9
|
+
* fix this rule offers on them points there. No other key resolves those names
|
|
10
|
+
* (`width: 'md'` is invalid CSS), so a px string anywhere else is pointed at a
|
|
11
|
+
* token (`hds.space.*` or a `var(--...)`). Bare numbers still resolve off the
|
|
12
|
+
* 4px space scale (`p: 4` is 16px), so this rule does NOT flag them — only
|
|
13
|
+
* values that prove the author reached past the token system (hex strings,
|
|
14
|
+
* explicit 'Npx' strings). Nested objects (responsive `{ xs, sm, ... }` and
|
|
15
|
+
* `&`-selector blocks) are walked recursively since sx supports both; a value
|
|
16
|
+
* in a responsive map is reported against the key the map sits on.
|
|
17
|
+
*/
|
|
18
|
+
import { attrName, jsxObjectExpression, propName, stringLiteralValue } from './utils.mjs';
|
|
19
|
+
|
|
20
|
+
const HEX_RE = /#[0-9a-fA-F]{3,4}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{8}\b/;
|
|
21
|
+
const PX_STRING_RE = /-?\d+(?:\.\d+)?px/;
|
|
22
|
+
|
|
23
|
+
// The Box sx keys whose values go through the spacing resolver, the only
|
|
24
|
+
// keys that take the t-shirt names (box-sx.ts SPACING_PROP_MAP).
|
|
25
|
+
const SPACING_KEYS = new Set([
|
|
26
|
+
'm',
|
|
27
|
+
'mt',
|
|
28
|
+
'mr',
|
|
29
|
+
'mb',
|
|
30
|
+
'ml',
|
|
31
|
+
'mx',
|
|
32
|
+
'my',
|
|
33
|
+
'p',
|
|
34
|
+
'pt',
|
|
35
|
+
'pr',
|
|
36
|
+
'pb',
|
|
37
|
+
'pl',
|
|
38
|
+
'px',
|
|
39
|
+
'py',
|
|
40
|
+
'gap',
|
|
41
|
+
'rowGap',
|
|
42
|
+
'columnGap',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'problem',
|
|
48
|
+
docs: {
|
|
49
|
+
description:
|
|
50
|
+
'Disallow raw hex/px string values inside Box sx — sx must resolve colors and spacing through HDS token keys.',
|
|
51
|
+
recommended: true,
|
|
52
|
+
url: 'https://github.com/hirobius/hds/blob/main/scripts/eslint-plugin-hds/README.md#hdssx-token-first',
|
|
53
|
+
},
|
|
54
|
+
schema: [],
|
|
55
|
+
messages: {
|
|
56
|
+
rawHexInSx:
|
|
57
|
+
'Raw hex color "{{value}}" in sx prop "{{prop}}" bypasses HDS color tokens. Use a dotted token key (e.g. "content.primary", "surface.raised", "accent").',
|
|
58
|
+
rawPxInSx:
|
|
59
|
+
'Raw px string "{{value}}" in sx prop "{{prop}}" bypasses the HDS spacing scale. Use a t-shirt step: "xs" | "sm" | "md" | "lg" | "xl" (8/16/24/32/48px at comfortable density; compact tightens them).',
|
|
60
|
+
rawPxInSxNonSpacing:
|
|
61
|
+
'Raw px string "{{value}}" in sx prop "{{prop}}" bypasses HDS tokens. Use a token: an hds.space.* value or a var(--...) such as "var(--primitive-space-4)". Spacing step names apply to the spacing props (p, m, gap, ...) only.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
create(context) {
|
|
65
|
+
/**
|
|
66
|
+
* @param objExpr an sx object, an `&`-selector block, or a responsive map
|
|
67
|
+
* @param mapKey the key a responsive map sits on (null for an sx object or
|
|
68
|
+
* an `&` block, whose keys are real sx keys)
|
|
69
|
+
*/
|
|
70
|
+
function walk(objExpr, mapKey = null) {
|
|
71
|
+
for (const prop of objExpr.properties) {
|
|
72
|
+
if (prop.type !== 'Property') continue; // skip SpreadElement
|
|
73
|
+
const name = propName(prop.key) ?? '?';
|
|
74
|
+
|
|
75
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
76
|
+
// '&'-selector nesting holds sx keys again; anything else is a
|
|
77
|
+
// responsive { xs, sm, ... } map on this key.
|
|
78
|
+
walk(prop.value, name.startsWith('&') ? null : (mapKey ?? name));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const value = stringLiteralValue(prop.value);
|
|
83
|
+
if (value == null) continue;
|
|
84
|
+
const key = mapKey ?? name;
|
|
85
|
+
|
|
86
|
+
if (HEX_RE.test(value)) {
|
|
87
|
+
context.report({
|
|
88
|
+
node: prop.value,
|
|
89
|
+
messageId: 'rawHexInSx',
|
|
90
|
+
data: { value, prop: key },
|
|
91
|
+
});
|
|
92
|
+
} else if (PX_STRING_RE.test(value)) {
|
|
93
|
+
context.report({
|
|
94
|
+
node: prop.value,
|
|
95
|
+
messageId: SPACING_KEYS.has(key) ? 'rawPxInSx' : 'rawPxInSxNonSpacing',
|
|
96
|
+
data: { value, prop: key },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
JSXAttribute(node) {
|
|
104
|
+
if (attrName(node) !== 'sx') return;
|
|
105
|
+
const obj = jsxObjectExpression(node.value);
|
|
106
|
+
if (!obj) return;
|
|
107
|
+
walk(obj);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST helpers for the HDS ESLint rules. Deliberately AST-based (not
|
|
3
|
+
* regex-over-source-text like the repo's internal scripts/check-*.mjs
|
|
4
|
+
* guardrails) because this plugin runs inside consumers' own ESLint configs
|
|
5
|
+
* against arbitrary TSX, where a plain parser is already available and gives
|
|
6
|
+
* us exact node types for free.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Resolve a JSXAttribute's name, e.g. `style` / `className` / `sx`. */
|
|
10
|
+
export function attrName(node) {
|
|
11
|
+
return node.name && node.name.type === 'JSXIdentifier' ? node.name.name : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolve an ObjectExpression Property key to a plain string, or null. */
|
|
15
|
+
export function propName(key) {
|
|
16
|
+
if (!key) return null;
|
|
17
|
+
if (key.type === 'Identifier') return key.name;
|
|
18
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a JSXAttribute's `{ ... }` expression to the ObjectExpression it
|
|
24
|
+
* wraps, e.g. `style={{ margin: 0 }}` -> the `{ margin: 0 }` ObjectExpression.
|
|
25
|
+
* Returns null for anything else (identifiers, spreads-only, conditional
|
|
26
|
+
* expressions, etc.) — this plugin only understands literal object props.
|
|
27
|
+
*/
|
|
28
|
+
export function jsxObjectExpression(attrValueNode) {
|
|
29
|
+
if (!attrValueNode || attrValueNode.type !== 'JSXExpressionContainer') return null;
|
|
30
|
+
const expr = attrValueNode.expression;
|
|
31
|
+
return expr && expr.type === 'ObjectExpression' ? expr : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a plain string literal value from a node — covers `'x'`/`"x"` and
|
|
36
|
+
* zero-interpolation template literals (`` `x` ``). Returns null for
|
|
37
|
+
* anything computed (identifiers, member expressions, interpolated
|
|
38
|
+
* templates) — this plugin only flags values it can prove are raw.
|
|
39
|
+
*/
|
|
40
|
+
export function stringLiteralValue(node) {
|
|
41
|
+
if (!node) return null;
|
|
42
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
43
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
44
|
+
return node.quasis.map((q) => q.value.cooked ?? '').join('');
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a numeric literal value from a node, including unary-minus
|
|
51
|
+
* (`-8`), which the estree parser represents as UnaryExpression, not a
|
|
52
|
+
* negative Literal. Returns null for anything else.
|
|
53
|
+
*/
|
|
54
|
+
export function numberLiteralValue(node) {
|
|
55
|
+
if (!node) return null;
|
|
56
|
+
if (node.type === 'Literal' && typeof node.value === 'number') return node.value;
|
|
57
|
+
if (
|
|
58
|
+
node.type === 'UnaryExpression' &&
|
|
59
|
+
node.operator === '-' &&
|
|
60
|
+
node.argument.type === 'Literal' &&
|
|
61
|
+
typeof node.argument.value === 'number'
|
|
62
|
+
) {
|
|
63
|
+
return -node.argument.value;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|