@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.
Files changed (175) hide show
  1. package/AGENTS.md +123 -0
  2. package/CONSUMING.md +11 -6
  3. package/DESIGN.md +34 -21
  4. package/NOTICE.md +1 -1
  5. package/README.md +48 -13
  6. package/codemods/hds-prefix.mjs +231 -0
  7. package/codemods/jsx-fold.mjs +515 -0
  8. package/codemods/not-found-pattern.mjs +71 -0
  9. package/codemods/patterns-subpath.mjs +242 -81
  10. package/codemods/patterns-subpath.names.json +41 -15
  11. package/codemods/removed-0.20.json +194 -0
  12. package/codemods/tile-grid.mjs +283 -0
  13. package/codemods/unrewritable.mjs +291 -0
  14. package/dist/chunks/DemoStateContext-Cz5TmrYu.js +32 -0
  15. package/dist/chunks/empty-state-DNoThgfV.js +1227 -0
  16. package/dist/chunks/form-By82qZcC.js +66 -0
  17. package/dist/contexts.js +157 -78
  18. package/dist/fonts/geist-mono-400.woff2 +0 -0
  19. package/dist/fonts/satoshi-400.woff2 +0 -0
  20. package/dist/fonts/satoshi-500.woff2 +0 -0
  21. package/dist/fonts/satoshi-700.woff2 +0 -0
  22. package/dist/fonts.css +53 -0
  23. package/dist/form.js +1 -1
  24. package/dist/hirobius-ui.js +1600 -5354
  25. package/dist/manifest.js +12 -13
  26. package/dist/patterns.js +796 -510
  27. package/dist/styles.css +2 -2
  28. package/dist/tokens.css +1 -1
  29. package/dist/tokens.js +165 -65
  30. package/dist/types/src/app/components/alert-dialog.d.ts +5 -0
  31. package/dist/types/src/app/components/asset-img.d.ts +1 -0
  32. package/dist/types/src/app/components/avatar-group.d.ts +2 -2
  33. package/dist/types/src/app/components/badge.d.ts +8 -3
  34. package/dist/types/src/app/components/blockquote.d.ts +2 -2
  35. package/dist/types/src/app/components/box-sx.d.ts +110 -13
  36. package/dist/types/src/app/components/box.d.ts +6 -3
  37. package/dist/types/src/app/components/breadcrumb.d.ts +0 -5
  38. package/dist/types/src/app/components/button.d.ts +35 -4
  39. package/dist/types/src/app/components/card.d.ts +10 -10
  40. package/dist/types/src/app/components/combobox.d.ts +47 -11
  41. package/dist/types/src/app/components/container.d.ts +2 -2
  42. package/dist/types/src/app/components/data-table-section.d.ts +2 -1
  43. package/dist/types/src/app/components/destructive-section.d.ts +1 -0
  44. package/dist/types/src/app/components/dialog.d.ts +8 -1
  45. package/dist/types/src/app/components/disclosure.d.ts +1 -5
  46. package/dist/types/src/app/components/error-pattern.d.ts +6 -1
  47. package/dist/types/src/app/components/field.d.ts +1 -2
  48. package/dist/types/src/app/components/form-actions.d.ts +1 -0
  49. package/dist/types/src/app/components/form-control.d.ts +10 -0
  50. package/dist/types/src/app/components/grid.d.ts +15 -6
  51. package/dist/types/src/app/components/hds-tooltip.d.ts +12 -2
  52. package/dist/types/src/app/components/icon.d.ts +1 -0
  53. package/dist/types/src/app/components/inline-code.d.ts +1 -2
  54. package/dist/types/src/app/components/input.d.ts +8 -5
  55. package/dist/types/src/app/components/kbd.d.ts +2 -2
  56. package/dist/types/src/app/components/metadata-list.d.ts +2 -2
  57. package/dist/types/src/app/components/metric-tiles.d.ts +1 -0
  58. package/dist/types/src/app/components/page-header.d.ts +0 -2
  59. package/dist/types/src/app/components/popover.d.ts +8 -0
  60. package/dist/types/src/app/components/progress.d.ts +33 -6
  61. package/dist/types/src/app/components/segmented-control.d.ts +2 -26
  62. package/dist/types/src/app/components/select.d.ts +10 -1
  63. package/dist/types/src/app/components/skeleton.d.ts +3 -4
  64. package/dist/types/src/app/components/spinner.d.ts +1 -2
  65. package/dist/types/src/app/components/stack.d.ts +4 -1
  66. package/dist/types/src/app/components/stat.d.ts +5 -5
  67. package/dist/types/src/app/components/status-list-item.d.ts +1 -2
  68. package/dist/types/src/app/components/status-tile.d.ts +3 -2
  69. package/dist/types/src/app/components/surface.d.ts +2 -3
  70. package/dist/types/src/app/components/switcher.d.ts +1 -1
  71. package/dist/types/src/app/components/table.d.ts +6 -15
  72. package/dist/types/src/app/components/tag.d.ts +0 -6
  73. package/dist/types/src/app/components/text.d.ts +54 -8
  74. package/dist/types/src/app/components/textarea.d.ts +1 -2
  75. package/dist/types/src/app/components/timestamp.d.ts +1 -0
  76. package/dist/types/src/app/components/toast.d.ts +1 -2
  77. package/dist/types/src/app/components/tooltip.d.ts +1 -1
  78. package/dist/types/src/app/context/DemoStateContext.d.ts +1 -1
  79. package/dist/types/src/app/context/RouterContext.d.ts +5 -5
  80. package/dist/types/src/app/data/manifest-types.d.ts +10 -5
  81. package/dist/types/src/app/design-system/generated-token-refs.d.ts +45 -28
  82. package/dist/types/src/app/design-system/generated-token-values.d.ts +4 -0
  83. package/dist/types/src/app/design-system/tokens.d.ts +139 -18
  84. package/dist/types/src/icons.d.ts +1 -1
  85. package/dist/types/src/index.d.ts +76 -164
  86. package/dist/types/src/patterns.d.ts +20 -25
  87. package/dist/variables.css +47 -33
  88. package/docs/CONSUMING.md +80 -48
  89. package/hirobius.tokens.json +125 -88
  90. package/llms.txt +166 -59
  91. package/mcp/catalog.mjs +366 -0
  92. package/mcp/guide.mjs +315 -0
  93. package/mcp/hds-mcp.mjs +49 -0
  94. package/mcp/server.mjs +94 -0
  95. package/package.json +48 -27
  96. package/public/hds-manifest.json +2667 -6705
  97. package/public/llms/components.txt +45 -426
  98. package/public/llms/layout.txt +5 -5
  99. package/public/llms-full.txt +245 -506
  100. package/public/llms.txt +166 -59
  101. package/scripts/eslint-plugin-hds/index.d.mts +29 -0
  102. package/scripts/eslint-plugin-hds/index.mjs +55 -0
  103. package/scripts/eslint-plugin-hds/package.json +30 -0
  104. package/scripts/eslint-plugin-hds/rules/no-raw-controls.mjs +47 -0
  105. package/scripts/eslint-plugin-hds/rules/no-raw-hex.mjs +70 -0
  106. package/scripts/eslint-plugin-hds/rules/no-raw-px-spacing.mjs +125 -0
  107. package/scripts/eslint-plugin-hds/rules/prefer-hds-layout-primitive.mjs +54 -0
  108. package/scripts/eslint-plugin-hds/rules/sx-token-first.mjs +111 -0
  109. package/scripts/eslint-plugin-hds/rules/utils.mjs +66 -0
  110. package/src/app/data/component-api.json +1082 -2913
  111. package/dist/chunks/LanguageContext-BxdCwiFg.js +0 -95
  112. package/dist/chunks/TokenDisplayContext-DKYJMVWk.js +0 -26
  113. package/dist/chunks/activity-feed-BGQ6v3wc.js +0 -22040
  114. package/dist/chunks/form-COP3vK9_.js +0 -66
  115. package/dist/types/src/app/components/activity-feed.d.ts +0 -73
  116. package/dist/types/src/app/components/app-shell.d.ts +0 -40
  117. package/dist/types/src/app/components/aspect-ratio.d.ts +0 -25
  118. package/dist/types/src/app/components/bleed.d.ts +0 -39
  119. package/dist/types/src/app/components/button-group.d.ts +0 -22
  120. package/dist/types/src/app/components/calendar.d.ts +0 -22
  121. package/dist/types/src/app/components/carousel.d.ts +0 -26
  122. package/dist/types/src/app/components/center.d.ts +0 -36
  123. package/dist/types/src/app/components/cinematic-link.d.ts +0 -18
  124. package/dist/types/src/app/components/circular-progress.d.ts +0 -28
  125. package/dist/types/src/app/components/cluster.d.ts +0 -39
  126. package/dist/types/src/app/components/command-palette.d.ts +0 -46
  127. package/dist/types/src/app/components/component-instance-matrix.d.ts +0 -27
  128. package/dist/types/src/app/components/context-menu.d.ts +0 -78
  129. package/dist/types/src/app/components/cover.d.ts +0 -43
  130. package/dist/types/src/app/components/date-input.d.ts +0 -31
  131. package/dist/types/src/app/components/date-range-input.d.ts +0 -34
  132. package/dist/types/src/app/components/date-time-input.d.ts +0 -30
  133. package/dist/types/src/app/components/doc-link-card.d.ts +0 -32
  134. package/dist/types/src/app/components/error-boundary.d.ts +0 -25
  135. package/dist/types/src/app/components/file-input.d.ts +0 -30
  136. package/dist/types/src/app/components/foundation-swatch.d.ts +0 -45
  137. package/dist/types/src/app/components/frame.d.ts +0 -38
  138. package/dist/types/src/app/components/heading-stack.d.ts +0 -52
  139. package/dist/types/src/app/components/history-card.d.ts +0 -12
  140. package/dist/types/src/app/components/hover-card.d.ts +0 -46
  141. package/dist/types/src/app/components/icon-button.d.ts +0 -22
  142. package/dist/types/src/app/components/image-lightbox.d.ts +0 -14
  143. package/dist/types/src/app/components/input-group.d.ts +0 -23
  144. package/dist/types/src/app/components/multi-selector.d.ts +0 -40
  145. package/dist/types/src/app/components/nav-group.d.ts +0 -29
  146. package/dist/types/src/app/components/nav-item.d.ts +0 -33
  147. package/dist/types/src/app/components/not-found-pattern.d.ts +0 -1
  148. package/dist/types/src/app/components/overflow-list.d.ts +0 -21
  149. package/dist/types/src/app/components/selectable-card.d.ts +0 -29
  150. package/dist/types/src/app/components/side-nav.d.ts +0 -23
  151. package/dist/types/src/app/components/sketch.d.ts +0 -19
  152. package/dist/types/src/app/components/stacked-card-rail.d.ts +0 -32
  153. package/dist/types/src/app/components/status-dot.d.ts +0 -25
  154. package/dist/types/src/app/components/stepper-field.d.ts +0 -12
  155. package/dist/types/src/app/components/stepper.d.ts +0 -33
  156. package/dist/types/src/app/components/text-lockup.d.ts +0 -38
  157. package/dist/types/src/app/components/tile-grid.d.ts +0 -17
  158. package/dist/types/src/app/components/time-input.d.ts +0 -24
  159. package/dist/types/src/app/components/toggle-button.d.ts +0 -32
  160. package/dist/types/src/app/components/token.d.ts +0 -90
  161. package/dist/types/src/app/components/tokenUtils.d.ts +0 -38
  162. package/dist/types/src/app/components/tokenizer.d.ts +0 -24
  163. package/dist/types/src/app/components/toolbar.d.ts +0 -36
  164. package/dist/types/src/app/components/top-nav.d.ts +0 -23
  165. package/dist/types/src/app/components/tree-list.d.ts +0 -30
  166. package/dist/types/src/app/data/nav-model.d.ts +0 -39
  167. package/dist/types/src/app/hooks/useFocusVisible.d.ts +0 -9
  168. package/dist/types/src/app/layouts/CaseStudyLayout.d.ts +0 -20
  169. package/dist/types/src/app/layouts/DocLayout.d.ts +0 -14
  170. package/dist/types/src/app/layouts/EmbeddedDocLayoutContext.d.ts +0 -6
  171. package/dist/types/src/app/layouts/HdsDocsShell.d.ts +0 -72
  172. package/dist/types/src/app/layouts/HdsSystemDocLayout.d.ts +0 -14
  173. package/dist/types/src/app/lib/hds-search.d.ts +0 -45
  174. package/dist/types/src/app/lib/nav-active.d.ts +0 -16
  175. 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
+ }