@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,194 @@
1
+ {
2
+ "$comment": "Every export 0.20.0 removed from the package root or @hirobius/design-system/patterns with no replacement (hds#389 R1 docs/lab components, hds#394 wave 4a and the root *Variants helpers), keyed by the source module that exported it. Hand-kept because the modules are gone; scripts/__tests__/patterns-subpath-removed.test.mjs fails if any listed name is exported from the root or /patterns again. codemods/patterns-subpath.mjs reports an import of a listed name for a manual edit instead of moving it. Names that moved (/patterns) or were renamed (Hds* aliases) are not here; MIGRATIONS.md '0.20.0 removals' has every row. `replaced` holds the names removed with a survivor (hds#394 wave 4b, and hds#395 B5 where a codemod of its own rewrites ops' sites), by module, each with the survivor the codemod's report names (\"use <survivor>\") instead of \"no replacement\".",
3
+ "release": "0.20.0",
4
+ "modules": {
5
+ "src/app/components/activity-feed.tsx": [
6
+ "activityAvatarVariants",
7
+ "ActivityEvent",
8
+ "ActivityFeed",
9
+ "ActivityFeedProps",
10
+ "ActivityStatus",
11
+ "ActivityTone",
12
+ "activityToneVariants",
13
+ "defaultActivityEvents"
14
+ ],
15
+ "src/app/components/app-shell.tsx": ["AppShell", "AppShellProps", "appShellVariants"],
16
+ "src/app/components/avatar-group.tsx": ["overflowBubbleVariants"],
17
+ "src/app/components/badge.tsx": ["badgeVariants"],
18
+ "src/app/components/blockquote.tsx": ["blockquoteVariants"],
19
+ "src/app/components/breadcrumb.tsx": ["crumbLabelVariants"],
20
+ "src/app/components/button-group.tsx": [
21
+ "ButtonGroup",
22
+ "ButtonGroupProps",
23
+ "buttonGroupVariants"
24
+ ],
25
+ "src/app/components/button.tsx": ["buttonVariants"],
26
+ "src/app/components/calendar.tsx": ["Calendar", "CalendarProps"],
27
+ "src/app/components/card.tsx": ["cardVariants"],
28
+ "src/app/components/carousel.tsx": ["Carousel", "carouselControlVariants", "CarouselProps"],
29
+ "src/app/components/cinematic-link.tsx": ["CinematicLink", "CinematicLinkProps"],
30
+ "src/app/components/circular-progress.tsx": ["circularProgressVariants"],
31
+ "src/app/components/command-palette.tsx": [
32
+ "cmdkDescriptionVariants",
33
+ "cmdkKindBadgeVariants",
34
+ "cmdkRowVariants",
35
+ "CommandPalette",
36
+ "CommandPaletteProps"
37
+ ],
38
+ "src/app/components/component-instance-matrix.tsx": ["ComponentInstanceMatrix"],
39
+ "src/app/components/context-menu.tsx": ["ContextMenu"],
40
+ "src/app/components/date-input.tsx": ["DateInput", "DateInputProps"],
41
+ "src/app/components/date-range-input.tsx": ["DateRangeInput", "DateRangeInputProps"],
42
+ "src/app/components/date-time-input.tsx": ["DateTimeInput", "DateTimeInputProps"],
43
+ "src/app/components/disclosure.tsx": ["disclosureTriggerVariants"],
44
+ "src/app/components/doc-link-card.tsx": [
45
+ "DocLinkCard",
46
+ "DocLinkCardProps",
47
+ "docLinkCardVariants"
48
+ ],
49
+ "src/app/components/error-boundary.tsx": ["ErrorBoundary", "ErrorBoundaryProps"],
50
+ "src/app/components/field.tsx": ["fieldValueVariants"],
51
+ "src/app/components/file-input.tsx": ["FileInput", "FileInputProps", "fileInputVariants"],
52
+ "src/app/components/foundation-swatch.tsx": ["FoundationSwatch", "FoundationSwatchProps"],
53
+ "src/app/components/heading-stack.tsx": [
54
+ "HeadingStack",
55
+ "headingStackLevelVariants",
56
+ "HeadingStackProps",
57
+ "headingStackVariants"
58
+ ],
59
+ "src/app/components/history-card.tsx": ["HistoryCard", "HistoryCardCommit", "HistoryCardProps"],
60
+ "src/app/components/hover-card.tsx": ["HoverCard"],
61
+ "src/app/components/image-lightbox.tsx": ["Lightbox", "LightboxProps"],
62
+ "src/app/components/inline-code.tsx": ["inlineCodeVariants"],
63
+ "src/app/components/input.tsx": ["inputVariants"],
64
+ "src/app/components/kbd.tsx": ["kbdVariants"],
65
+ "src/app/components/metadata-list.tsx": ["metadataListVariants"],
66
+ "src/app/components/nav-group.tsx": ["NavGroup", "navGroupLabelVariants", "NavGroupProps"],
67
+ "src/app/components/nav-item.tsx": [
68
+ "navIndicatorVariants",
69
+ "NavItem",
70
+ "navItemVariants",
71
+ "NavProps"
72
+ ],
73
+ "src/app/components/overflow-list.tsx": ["OverflowList", "OverflowListProps"],
74
+ "src/app/components/progress.tsx": ["progressTrackVariants"],
75
+ "src/app/components/segmented-control.tsx": [
76
+ "segmentedControlDescriptionVariants",
77
+ "segmentedControlFocusRingVariants",
78
+ "segmentedControlIndicatorVariants",
79
+ "segmentedControlItemVariants",
80
+ "segmentedControlLabelVariants",
81
+ "segmentedControlRailVariants",
82
+ "segmentedControlWrapperVariants"
83
+ ],
84
+ "src/app/components/selectable-card.tsx": ["selectableCardVariants"],
85
+ "src/app/components/side-nav.tsx": [
86
+ "SideNav",
87
+ "SideNavLevel",
88
+ "SideNavProps",
89
+ "sideNavVariants"
90
+ ],
91
+ "src/app/components/skeleton.tsx": ["skeletonVariants"],
92
+ "src/app/components/sketch.tsx": ["Sketch", "SketchProps"],
93
+ "src/app/components/spinner.tsx": ["spinnerVariants"],
94
+ "src/app/components/stacked-card-rail.tsx": [
95
+ "StackedCardRail",
96
+ "StackedCardRailCard",
97
+ "StackedCardRailProps"
98
+ ],
99
+ "src/app/components/stat.tsx": ["statVariants"],
100
+ "src/app/components/status-dot.tsx": ["statusDotVariants"],
101
+ "src/app/components/status-list-item.tsx": ["statusListItemDotVariants"],
102
+ "src/app/components/stepper-field.tsx": ["StepperField", "StepperFieldProps"],
103
+ "src/app/components/stepper.tsx": ["Step", "stepMarkerVariants", "Stepper", "StepperProps"],
104
+ "src/app/components/surface.tsx": ["surfaceVariants"],
105
+ "src/app/components/table.tsx": [
106
+ "tableDataCellVariants",
107
+ "tableHeaderCellVariants",
108
+ "tableSortButtonVariants"
109
+ ],
110
+ "src/app/components/tag.tsx": ["tagButtonVariants", "tagPillVariants"],
111
+ "src/app/components/text-lockup.tsx": ["TextLockup", "TextLockupProps"],
112
+ "src/app/components/text.tsx": ["textVariants"],
113
+ "src/app/components/textarea.tsx": ["textareaVariants"],
114
+ "src/app/components/time-input.tsx": ["hdsTimeInputVariants"],
115
+ "src/app/components/toast.tsx": ["toastIconVariants"],
116
+ "src/app/components/toggle-button.tsx": ["hdsToggleButtonVariants"],
117
+ "src/app/components/token.tsx": [
118
+ "Token",
119
+ "tokenLabelVariants",
120
+ "tokenNodeInlineVariants",
121
+ "TokenProps",
122
+ "tokenShellVariants"
123
+ ],
124
+ "src/app/components/tokenizer.tsx": ["Tokenizer", "TokenizerProps"],
125
+ "src/app/components/toolbar.tsx": ["Toolbar", "ToolbarComponent"],
126
+ "src/app/components/top-nav.tsx": ["TopNav", "TopNavProps"],
127
+ "src/app/components/tree-list.tsx": ["TreeList", "TreeListProps", "TreeNode"],
128
+ "src/app/layouts/CaseStudyLayout.tsx": ["CaseStudyLayout", "CaseStudyLayoutProps"],
129
+ "src/app/layouts/HdsDocsShell.tsx": ["HdsDocsShell", "HdsDocsShellProps"],
130
+ "src/app/layouts/HdsSystemDocLayout.tsx": ["HdsSystemDocLayout"]
131
+ },
132
+ "replaced": {
133
+ "src/app/components/aspect-ratio.tsx": {
134
+ "AspectRatio": "Box style={{ aspectRatio: '16 / 9' }}",
135
+ "AspectRatioProps": "BoxProps"
136
+ },
137
+ "src/app/components/bleed.tsx": {
138
+ "Bleed": "Box style={{ marginInline: 'calc(-1 * var(--semantic-space-scale-md))' }}",
139
+ "BleedProps": "BoxProps"
140
+ },
141
+ "src/app/components/center.tsx": {
142
+ "Center": "Container maxWidth=\"content\", with a Box style={{ paddingInline }} inside for the gutter",
143
+ "CenterProps": "ContainerProps"
144
+ },
145
+ "src/app/components/circular-progress.tsx": {
146
+ "CircularProgress": "Progress variant=\"circular\"",
147
+ "CircularProgressProps": "ProgressProps"
148
+ },
149
+ "src/app/components/cluster.tsx": {
150
+ "Cluster": "Stack direction=\"row\" wrap=\"wrap\" align=\"center\"",
151
+ "ClusterProps": "StackProps"
152
+ },
153
+ "src/app/components/cover.tsx": {
154
+ "Cover": "Box style={{ display: 'flex', flexDirection: 'column', minHeight: '100svh' }} around a Box style={{ marginBlock: 'auto' }}",
155
+ "CoverProps": "BoxProps"
156
+ },
157
+ "src/app/components/frame.tsx": {
158
+ "Frame": "Box style={{ aspectRatio: '16 / 9', overflow: 'hidden', borderRadius: hds.borderRadius.md }}",
159
+ "FrameProps": "BoxProps"
160
+ },
161
+ "src/app/components/icon-button.tsx": {
162
+ "IconButton": "Button iconOnly label=\"…\" iconLeft={<Icon icon={…} />}",
163
+ "IconButtonProps": "ButtonProps"
164
+ },
165
+ "src/app/components/input-group.tsx": {
166
+ "InputGroup": "Input prefix / suffix",
167
+ "InputGroupProps": "InputProps"
168
+ },
169
+ "src/app/components/multi-selector.tsx": {
170
+ "MultiSelector": "Combobox multiple",
171
+ "MultiSelectorOption": "ComboboxOption",
172
+ "MultiSelectorProps": "ComboboxMultipleProps"
173
+ },
174
+ "src/app/components/not-found-pattern.tsx": {
175
+ "NotFoundPattern": "ErrorPattern displayText=\"404\" message=\"Page not found\" from @hirobius/design-system/patterns (hds-not-found-pattern rewrites it)"
176
+ },
177
+ "src/app/components/selectable-card.tsx": {
178
+ "SelectableCard": "Card selectable selected onSelectedChange",
179
+ "SelectableCardProps": "CardProps"
180
+ },
181
+ "src/app/components/tile-grid.tsx": {
182
+ "TileGrid": "Grid layout=\"auto-fill\" minItemWidth=\"…\" gap=\"medium\" (hds-tile-grid rewrites it)",
183
+ "TileGridProps": "GridProps (minTileWidth is minItemWidth, gap 'sm' is 'medium')"
184
+ },
185
+ "src/app/components/time-input.tsx": {
186
+ "TimeInput": "Input type=\"time\"",
187
+ "TimeInputProps": "InputProps"
188
+ },
189
+ "src/app/components/toggle-button.tsx": {
190
+ "ToggleButton": "Button pressed onPressedChange (variant=\"ghost\" is variant=\"tertiary\")",
191
+ "ToggleButtonProps": "ButtonProps"
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tile-grid codemod (hds#395, hds#389's 2026-10-01 decision update)
4
+ *
5
+ * 0.20.0 removed TileGrid from the package root. Grid renders its recipe since
6
+ * hds#393: `layout="auto-fill"` with `minItemWidth`, and the fixed 12px
7
+ * `medium` gap step. TileGrid defaulted to 260px tiles on a 12px gap; Grid
8
+ * defaults to 280px and 32px, so the rewrite always writes both:
9
+ *
10
+ * <TileGrid minTileWidth="280px"> <Grid layout="auto-fill" minItemWidth="280px" gap="medium">
11
+ * <TileGrid minTileWidth="260px" gap="sm"> <Grid layout="auto-fill" minItemWidth="260px" gap="medium">
12
+ * <TileGrid> <Grid layout="auto-fill" minItemWidth="260px" gap="medium">
13
+ *
14
+ * The rendered tracks and gap are the same under every tenant and density
15
+ * (scripts/__tests__/grid-tile-grid-parity.test.mjs). An expression width keeps
16
+ * TileGrid's fallback where it can be undefined: `minTileWidth={w}` becomes
17
+ * `minItemWidth={w ?? '260px'}`. One that never is (a string or template
18
+ * literal, a conditional whose branches all are) is written through as it is,
19
+ * since TypeScript rejects `??` on it (TS2869). The codemod reads that the way
20
+ * TypeScript does, from the syntax.
21
+ * Every other attribute, comment and line break stays, and the import names Grid
22
+ * (an alias stays the file's name; a file that binds Grid to something else keeps
23
+ * `Grid as TileGrid`; a file that already imports Grid reuses it).
24
+ *
25
+ * Left for a manual edit (codemods/jsx-fold.mjs): `gap="xs"` and `gap="md"`,
26
+ * which were fixed 8px and 16px where Grid's nearest steps (`tight`, `normal`)
27
+ * tighten under compact density; a gap or a spread the codemod cannot read; a
28
+ * width that is always undefined, or one it does not read (an operator other
29
+ * than a conditional or a logical one, a cast, an arrow); a self-closing tag
30
+ * (Grid needs children); any use that is not a JSX tag; and `TileGridProps`.
31
+ * Running it twice changes nothing.
32
+ *
33
+ * node codemods/tile-grid.mjs [--root <dir>] [--check] [--dry-run]
34
+ *
35
+ * --root <dir> directory to scan (default: current directory)
36
+ * --check write nothing; exit 1 while a rewrite is needed or a site needs
37
+ * a manual edit (a namespace or dynamic import that reads the
38
+ * name, or a file that cannot be read to its end, included)
39
+ * --dry-run write nothing; print each changed line before (-) and after (+), exit 0
40
+ */
41
+ import { ROOT_PKG, foldComponent, insertionPoint, isEntry, main, runFold } from './jsx-fold.mjs';
42
+ import { maskSource } from './unrewritable.mjs';
43
+
44
+ /** TileGrid's default width, which Grid does not share. */
45
+ const TILE_WIDTH = '260px';
46
+
47
+ /** TileGrid's fixed gaps Grid has no fixed step for, and Grid's nearest density-aware one. */
48
+ const NO_STEP = {
49
+ xs: '8px, and gap="tight" is 8px but 6px under compact density',
50
+ md: '16px, and gap="normal" is 16px but 12px under compact density',
51
+ };
52
+
53
+ // TypeScript's syntactic nullishness of an expression (checker.ts
54
+ // getSyntacticNullishnessSemantics), which decides TS2869 and TS2871 on `x ?? y`.
55
+ const ALWAYS = 1;
56
+ const NEVER = 2;
57
+ const SOMETIMES = ALWAYS | NEVER;
58
+
59
+ const OPEN = '([{';
60
+ const CLOSE = ')]}';
61
+
62
+ /** The bracket that closes the one at `at` in masked code, or -1. */
63
+ function closing(m, at) {
64
+ let depth = 0;
65
+ for (let i = at; i < m.length; i++) {
66
+ if (OPEN.includes(m[i])) depth++;
67
+ else if (CLOSE.includes(m[i]) && --depth === 0) return i;
68
+ }
69
+ return -1;
70
+ }
71
+
72
+ /** The backtick that closes the template whose opening backtick is at `at`, or -1. */
73
+ function templateEnd(m, at) {
74
+ let depth = 0;
75
+ for (let i = at + 1; i < m.length; i++) {
76
+ if (m[i] === '{') depth++;
77
+ else if (m[i] === '}') depth--;
78
+ else if (m[i] === '`' && depth === 0) return i;
79
+ }
80
+ return -1;
81
+ }
82
+
83
+ /**
84
+ * The operators of masked code outside brackets that decide its shape: `,`,
85
+ * `=>`, an assignment, the conditional's `?` and `:`, `??`, `||`, `&&`, and the
86
+ * `as` and `satisfies` keywords.
87
+ */
88
+ function topLevel(m) {
89
+ const ops = [];
90
+ let depth = 0;
91
+ for (let i = 0; i < m.length; i++) {
92
+ const ch = m[i];
93
+ if (OPEN.includes(ch)) depth++;
94
+ else if (CLOSE.includes(ch)) depth--;
95
+ else if (depth > 0) continue;
96
+ else if (ch === ',') ops.push({ at: i, op: ',' });
97
+ else if (ch === '=') {
98
+ if (m[i + 1] === '>') ops.push({ at: i++, op: '=>' });
99
+ else if (m[i + 1] === '=') while (m[i + 1] === '=') i++;
100
+ else if (
101
+ m[i - 1] === '!' ||
102
+ ((m[i - 1] === '<' || m[i - 1] === '>') && m[i - 2] !== m[i - 1])
103
+ )
104
+ continue;
105
+ else ops.push({ at: i, op: '=' });
106
+ } else if (ch === '?') {
107
+ if (m[i + 1] === '?') {
108
+ ops.push({ at: i, op: m[i + 2] === '=' ? '=' : '??' });
109
+ i += m[i + 2] === '=' ? 2 : 1;
110
+ } else if (m[i + 1] === '.' && !/\d/.test(m[i + 2] ?? '')) i++;
111
+ else ops.push({ at: i, op: '?' });
112
+ } else if (ch === ':') ops.push({ at: i, op: ':' });
113
+ else if ((ch === '|' || ch === '&') && m[i + 1] === ch) {
114
+ ops.push({ at: i, op: m[i + 2] === '=' ? '=' : '||' });
115
+ i += m[i + 2] === '=' ? 2 : 1;
116
+ } else if (/[\w$]/.test(ch) && !/[\w$.]/.test(m[i - 1] ?? '')) {
117
+ const word = /^[\w$]+/.exec(m.slice(i))[0];
118
+ if (word === 'as' || word === 'satisfies') ops.push({ at: i, op: 'as' });
119
+ i += word.length - 1;
120
+ }
121
+ }
122
+ return ops;
123
+ }
124
+
125
+ /**
126
+ * TypeScript's nullishness of a name, a member access, a call, an element
127
+ * access or a tagged template (each optional), with any `!`; null for anything
128
+ * else. `chain`: it binds tighter than `??`, so it takes no parentheses.
129
+ */
130
+ function chainNullishness(m) {
131
+ const id = /^[A-Za-z_$][\w$]*/.exec(m);
132
+ const group = m[0] === '(' ? closing(m, 0) : -1;
133
+ if (!id && group < 0) return null;
134
+ const base = id ? id[0] : m.slice(1, group);
135
+ let i = id ? base.length : group + 1;
136
+ let access = false;
137
+ while (i < m.length) {
138
+ const [step, optional] = /^\s*(\?\.)?\s*/.exec(m.slice(i));
139
+ const at = i + step.length;
140
+ const rest = m.slice(at);
141
+ const prop = (optional ? /^[A-Za-z_$][\w$]*/ : /^\.\s*[A-Za-z_$][\w$]*/).exec(rest);
142
+ if (prop) i = at + prop[0].length;
143
+ else if (rest[0] === '[' || rest[0] === '(') i = closing(m, at) + 1;
144
+ else if (rest[0] === '`' && !optional) i = templateEnd(m, at) + 1;
145
+ else if (rest[0] === '!' && rest[1] !== '=' && !optional) {
146
+ i = at + 1;
147
+ continue;
148
+ } else return null;
149
+ if (i === 0) return null; // closing() or templateEnd() found no end
150
+ access = true;
151
+ }
152
+ if (access) return { nullish: SOMETIMES, chain: true };
153
+ if (!id) {
154
+ const inner = nullishness(base);
155
+ return inner && { nullish: inner.nullish, chain: true };
156
+ }
157
+ if (base === 'undefined' || base === 'null') return { nullish: ALWAYS, chain: true };
158
+ if (base === 'true' || base === 'false') return { nullish: NEVER, chain: true };
159
+ if (/^(?:typeof|void|delete|await|new|yield|function|class)$/.test(base)) return null;
160
+ return { nullish: SOMETIMES, chain: true };
161
+ }
162
+
163
+ /**
164
+ * Whether an expression (masked code: strings, templates and comments blanked)
165
+ * can be null or undefined, the way TypeScript reads it from the syntax:
166
+ * ALWAYS, NEVER or SOMETIMES, or null where the codemod does not read it (an
167
+ * assignment, an arrow, a comma, a cast, an operator other than a conditional
168
+ * or a logical one). `chain`: it needs no parentheses in front of `??`.
169
+ * @param {string} code
170
+ * @returns {{ nullish: number, chain: boolean } | null}
171
+ */
172
+ function nullishness(code) {
173
+ let m = code.trim();
174
+ while (m[0] === '(' && closing(m, 0) === m.length - 1) m = m.slice(1, -1).trim();
175
+ if (m === '') return null;
176
+ const ops = topLevel(m);
177
+ if (ops.some((o) => o.op === ',' || o.op === '=>' || o.op === '=')) return null;
178
+ const q = ops.findIndex((o) => o.op === '?');
179
+ if (q >= 0) {
180
+ // The conditional's own `:`, past any nested conditional in its true branch.
181
+ let colon = -1;
182
+ for (let k = q + 1, open = 0; k < ops.length && colon < 0; k++)
183
+ if (ops[k].op === '?') open++;
184
+ else if (ops[k].op === ':' && open-- === 0) colon = ops[k].at;
185
+ if (colon < 0 || ops[q].at === 0) return null;
186
+ const yes = nullishness(m.slice(ops[q].at + 1, colon));
187
+ const no = nullishness(m.slice(colon + 1));
188
+ return yes && no && { nullish: yes.nullish | no.nullish, chain: false };
189
+ }
190
+ if (ops.some((o) => o.op === 'as' || o.op === ':')) return null;
191
+ const coalesce = ops.filter((o) => o.op === '??').pop();
192
+ if (coalesce) {
193
+ const right = nullishness(m.slice(coalesce.at + 2));
194
+ return right && { nullish: right.nullish, chain: false };
195
+ }
196
+ if (ops.some((o) => o.op === '||')) return { nullish: SOMETIMES, chain: false };
197
+ if (/^(['"])\s*\1$/.test(m)) return { nullish: NEVER, chain: true };
198
+ if (m[0] === '`' && templateEnd(m, 0) === m.length - 1) return { nullish: NEVER, chain: true };
199
+ return chainNullishness(m);
200
+ }
201
+
202
+ /** @type {import('./jsx-fold.mjs').FoldRule} */
203
+ export const RULE = Object.freeze({
204
+ name: 'TileGrid',
205
+ survivor: 'Grid',
206
+ survivorFrom: ROOT_PKG,
207
+ types: { TileGridProps: "GridProps: minTileWidth is minItemWidth, gap 'sm' is 'medium'" },
208
+ rewrite(tag, source) {
209
+ if (tag.selfClosing && !tag.attrs.some((a) => a.name === 'children'))
210
+ return { manual: 'Grid needs children; rewrite it by hand' };
211
+ if (tag.attrs.some((a) => a.kind === 'spread'))
212
+ return { manual: 'a spread can carry minTileWidth or gap; rewrite it by hand' };
213
+ const gap = tag.attrs.find((a) => a.name === 'gap');
214
+ if (gap && !(gap.kind === 'string' && gap.value === 'sm')) {
215
+ const text = source.slice(gap.start, gap.end);
216
+ const why = gap.kind === 'string' && NO_STEP[gap.value];
217
+ return {
218
+ manual: why
219
+ ? `${text}: TileGrid's gap was a fixed ${why}; rewrite it by hand`
220
+ : `${text}: the codemod cannot read the gap; rewrite it by hand`,
221
+ };
222
+ }
223
+ const width = tag.attrs.find((a) => a.name === 'minTileWidth');
224
+ if (width && width.kind === 'bare')
225
+ return { manual: 'minTileWidth has no value; rewrite it by hand' };
226
+ const edits = [];
227
+ if (gap) edits.push({ start: gap.valueStart + 1, end: gap.valueEnd - 1, text: 'medium' });
228
+ if (width) {
229
+ const { sep } = insertionPoint(source, tag, width);
230
+ edits.push(
231
+ { start: width.start, end: width.start, text: `layout="auto-fill"${sep}` },
232
+ { start: width.start, end: width.start + 'minTileWidth'.length, text: 'minItemWidth' },
233
+ );
234
+ if (width.kind === 'expr') {
235
+ const { code, unterminated } = maskSource(width.value);
236
+ const kind = unterminated ? null : nullishness(code);
237
+ const text = source.slice(width.start, width.end);
238
+ if (!kind)
239
+ return {
240
+ manual: `the codemod cannot tell whether ${text} can be undefined, where TileGrid drew ${TILE_WIDTH} tiles and Grid draws 280px; rewrite it by hand`,
241
+ };
242
+ if (kind.nullish === ALWAYS)
243
+ return {
244
+ manual: `${text} is always null or undefined, which drew TileGrid's ${TILE_WIDTH} tiles; write minItemWidth="${TILE_WIDTH}" by hand`,
245
+ };
246
+ if (kind.nullish === SOMETIMES) {
247
+ // In masked code a comment is blank, so the expression's own text starts
248
+ // and ends at its first and last non-blank character.
249
+ const first = width.valueStart + 1 + code.search(/\S/);
250
+ const last = width.valueStart + 1 + code.trimEnd().length;
251
+ const fallback = ` ?? '${TILE_WIDTH}'`;
252
+ if (kind.chain) edits.push({ start: last, end: last, text: fallback });
253
+ else
254
+ edits.push(
255
+ { start: first, end: first, text: '(' },
256
+ { start: last, end: last, text: `)${fallback}` },
257
+ );
258
+ }
259
+ }
260
+ if (!gap) edits.push({ start: width.end, end: width.end, text: `${sep}gap="medium"` });
261
+ } else {
262
+ const { at, sep } = insertionPoint(source, tag, null);
263
+ const attrs = ['layout="auto-fill"', `minItemWidth="${TILE_WIDTH}"`];
264
+ if (!gap) attrs.push('gap="medium"');
265
+ edits.push({ start: at, end: at, text: attrs.map((a) => `${sep}${a}`).join('') });
266
+ }
267
+ return { edits };
268
+ },
269
+ });
270
+
271
+ /** Pure transform of one file's source (codemods/jsx-fold.mjs `foldComponent`). */
272
+ export function transformSource(source) {
273
+ return foldComponent(source, RULE);
274
+ }
275
+
276
+ /** Scan a directory. Writes only when `write` is true. */
277
+ export function runCodemod({ root, write = false }) {
278
+ return runFold({ root, write, rule: RULE });
279
+ }
280
+
281
+ if (isEntry(import.meta.url)) {
282
+ process.exit(main(process.argv.slice(2), { bin: 'hds-tile-grid', rule: RULE }));
283
+ }