inkbridge 0.1.0-beta.2 → 0.1.0-beta.21

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 (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -0,0 +1,480 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import { applyShadcnAdapter, getShadcnSlotInjections } from '../src/framework-adapters/shadcn';
4
+ import type { NodeIR } from '../src/tailwind/node-ir';
5
+
6
+ /**
7
+ * Regression: `src/framework-adapters/shadcn.ts` is the single source of
8
+ * truth for class-list patches that translate shadcn / base-ui runtime
9
+ * CSS into Tailwind classes the plugin can see — patches the consumer's
10
+ * JSX *should* have had if those frameworks didn't use a stylesheet to
11
+ * do half the layout.
12
+ *
13
+ * This file locks the contract:
14
+ * 1. The registry has the slots we expect (so a casual edit can't
15
+ * silently delete `avatar-image`'s `absolute inset-0` patch).
16
+ * 2. `applyShadcnAdapter` actually injects them onto the matching node.
17
+ * 3. Non-matching nodes are returned by reference (cache-friendly).
18
+ * 4. Recursion reaches into nested children.
19
+ * 5. Existing classes are preserved; duplicates are de-duped.
20
+ *
21
+ * Adding a new slot? Add an entry here AND in
22
+ * `src/framework-adapters/shadcn.ts:SLOT_CLASS_INJECTIONS` AND a brief
23
+ * note in `tools/figma-plugin/.ai/framework-adapters.md`. All three
24
+ * together — registry, test, doc — keep this from drifting.
25
+ *
26
+ * Adding a whole new framework adapter (MUI, Headless UI, ...) is a new
27
+ * file under `src/framework-adapters/`, one line in the dispatcher
28
+ * `index.ts`, and a sibling regression file mirroring this one.
29
+ */
30
+
31
+ function el(props: Record<string, string>, classes: string[] = [], children: NodeIR[] = []): NodeIR {
32
+ return {
33
+ kind: 'element',
34
+ tagName: 'div',
35
+ tagLower: 'div',
36
+ props,
37
+ classes,
38
+ children,
39
+ } as unknown as NodeIR;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Registry contents
44
+ // ---------------------------------------------------------------------------
45
+
46
+ // avatar-image: base-ui pins `position: absolute; inset: 0` via stylesheet.
47
+ // We can't see CSS, so we re-add the equivalent Tailwind classes here.
48
+ {
49
+ const extras = getShadcnSlotInjections('avatar-image');
50
+ assert.ok(extras, 'avatar-image slot must be registered');
51
+ assert.deepEqual(
52
+ [...(extras as readonly string[])],
53
+ ['absolute', 'inset-0'],
54
+ 'avatar-image injection must be ["absolute", "inset-0"]',
55
+ );
56
+ }
57
+
58
+ // Unknown slots: null (so the recursion is a no-op).
59
+ assert.equal(getShadcnSlotInjections('not-a-real-slot'), null, 'unknown slot returns null');
60
+ assert.equal(getShadcnSlotInjections(''), null, 'empty slot returns null');
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // applyShadcnAdapter — class injection on the matching node
64
+ // ---------------------------------------------------------------------------
65
+
66
+ // (a) Matching slot: classes get the registered patches appended.
67
+ {
68
+ const before = el({ 'data-slot': 'avatar-image', src: '/x.svg' }, ['aspect-square', 'size-full']);
69
+ const after = applyShadcnAdapter(before);
70
+ assert.notEqual(after, before, 'matching slot returns a new node (immutable update)');
71
+ const afterEl = after as Extract<NodeIR, { kind: 'element' }>;
72
+ assert.deepEqual(
73
+ afterEl.classes,
74
+ ['aspect-square', 'size-full', 'absolute', 'inset-0'],
75
+ 'existing classes preserved; injection appended in order',
76
+ );
77
+ }
78
+
79
+ // (b) Already has the classes: no duplication, same-reference return.
80
+ {
81
+ const before = el(
82
+ { 'data-slot': 'avatar-image', src: '/x.svg' },
83
+ ['absolute', 'inset-0', 'size-full'],
84
+ );
85
+ const after = applyShadcnAdapter(before);
86
+ assert.equal(
87
+ after,
88
+ before,
89
+ 'when all injection classes are already present, return same reference (cache-friendly)',
90
+ );
91
+ }
92
+
93
+ // (c) Element without data-slot: untouched.
94
+ {
95
+ const before = el({}, ['flex', 'gap-4']);
96
+ const after = applyShadcnAdapter(before);
97
+ assert.equal(after, before, 'no data-slot → same reference');
98
+ }
99
+
100
+ // (d) Element with unknown data-slot: untouched.
101
+ {
102
+ const before = el({ 'data-slot': 'unknown-slot' }, ['flex']);
103
+ const after = applyShadcnAdapter(before);
104
+ assert.equal(after, before, 'unrecognised slot → same reference');
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Recursion — reaches into children, returns same ref when nothing changes
109
+ // ---------------------------------------------------------------------------
110
+
111
+ // (e) Avatar tree with a real image src: Image gets the injection, Fallback is
112
+ // DROPPED (shadcn's runtime hides Fallback when Image loads — we can't observe
113
+ // load state but we can observe "there is a real-looking src").
114
+ {
115
+ const image = el({ 'data-slot': 'avatar-image', src: '/x.svg' }, ['aspect-square', 'size-full']);
116
+ const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
117
+ const root = el(
118
+ { 'data-slot': 'avatar' },
119
+ ['relative', 'flex', 'size-10', 'rounded-full'],
120
+ [image, fallback],
121
+ );
122
+
123
+ const next = applyShadcnAdapter(root);
124
+ assert.notEqual(next, root, 'tree with a child injection / fallback drop produces a new root');
125
+
126
+ const nextRoot = next as Extract<NodeIR, { kind: 'element' }>;
127
+ const rootEl = root as Extract<NodeIR, { kind: 'element' }>;
128
+ // Root's own classes unchanged (avatar slot itself has no injection).
129
+ assert.deepEqual(
130
+ nextRoot.classes,
131
+ rootEl.classes,
132
+ 'avatar-root classes unchanged (no slot registration for it)',
133
+ );
134
+
135
+ // Fallback dropped — only the image remains.
136
+ assert.equal(
137
+ nextRoot.children.length,
138
+ 1,
139
+ 'avatar-fallback dropped when avatar-image has a real src — only image remains',
140
+ );
141
+
142
+ // Image child has injection.
143
+ const nextImage = nextRoot.children[0] as Extract<NodeIR, { kind: 'element' }>;
144
+ assert.deepEqual(
145
+ nextImage.classes,
146
+ ['aspect-square', 'size-full', 'absolute', 'inset-0'],
147
+ 'avatar-image child receives [absolute, inset-0] injection',
148
+ );
149
+ }
150
+
151
+ // (e2) Avatar tree WITHOUT an image (or image without src): Fallback survives.
152
+ {
153
+ const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
154
+ const root = el(
155
+ { 'data-slot': 'avatar' },
156
+ ['relative', 'flex', 'size-10', 'rounded-full'],
157
+ [fallback],
158
+ );
159
+
160
+ const next = applyShadcnAdapter(root);
161
+ assert.equal(next, root, 'no avatar-image → fallback kept, tree returned by reference');
162
+ }
163
+
164
+ // (e3) Avatar tree with an image whose src is just a placeholder string: Fallback survives.
165
+ // This guards against false-positive drops when the consumer JSX has
166
+ // `src={undefined}` (some bundlers emit the literal "src" token) or
167
+ // `src=""`. The looksLikeImageSrc predicate must agree with the renderer's.
168
+ {
169
+ const image = el({ 'data-slot': 'avatar-image', src: '' }, ['aspect-square', 'size-full']);
170
+ const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
171
+ const root = el(
172
+ { 'data-slot': 'avatar' },
173
+ ['relative', 'flex', 'size-10', 'rounded-full'],
174
+ [image, fallback],
175
+ );
176
+
177
+ const next = applyShadcnAdapter(root);
178
+ const nextRoot = next as Extract<NodeIR, { kind: 'element' }>;
179
+ // Image still gets its class injection.
180
+ // Fallback survives because the image src isn't a fetchable path.
181
+ assert.equal(nextRoot.children.length, 2, 'empty src → fallback survives');
182
+ }
183
+
184
+ // (f) Tree with no matching slots anywhere: same reference all the way down.
185
+ {
186
+ const root = el({}, ['flex'], [el({}, ['gap-4']), el({}, ['p-4'])]);
187
+ const next = applyShadcnAdapter(root);
188
+ assert.equal(next, root, 'tree with no matches → same reference (no allocation)');
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Progress: indicator width derived from Root's `value` prop
193
+ // ---------------------------------------------------------------------------
194
+
195
+ function findIndicator(root: NodeIR): Extract<NodeIR, { kind: 'element' }> | null {
196
+ const elNode = root as Extract<NodeIR, { kind: 'element' }>;
197
+ if (elNode.props && elNode.props['data-slot'] === 'progress-indicator') {
198
+ return elNode;
199
+ }
200
+ if (!('children' in elNode) || !Array.isArray(elNode.children)) return null;
201
+ for (const c of elNode.children) {
202
+ const r = findIndicator(c);
203
+ if (r) return r;
204
+ }
205
+ return null;
206
+ }
207
+
208
+ function progressTree(value: string | undefined): NodeIR {
209
+ const props: Record<string, string> = { 'data-slot': 'progress' };
210
+ if (value !== undefined) props.value = value;
211
+ const indicator = el(
212
+ { 'data-slot': 'progress-indicator' },
213
+ ['h-full', 'bg-primary', 'transition-all'],
214
+ );
215
+ const track = el(
216
+ { 'data-slot': 'progress-track' },
217
+ ['relative', 'h-2', 'w-full', 'overflow-hidden', 'rounded-full', 'bg-primary/20'],
218
+ [indicator],
219
+ );
220
+ return el(props, ['flex', 'w-full', 'flex-col', 'gap-2'], [track]);
221
+ }
222
+
223
+ // (g) value="60" → indicator gets w-[60%]
224
+ {
225
+ const root = progressTree('60');
226
+ const next = applyShadcnAdapter(root);
227
+ const indicator = findIndicator(next);
228
+ assert.ok(indicator, 'progress-indicator must still exist after adapter');
229
+ assert.ok(
230
+ indicator!.classes.includes('w-[60%]'),
231
+ 'value="60" → w-[60%] injected on progress-indicator',
232
+ );
233
+ }
234
+
235
+ // (g2) value="100" → w-[100%]
236
+ {
237
+ const root = progressTree('100');
238
+ const indicator = findIndicator(applyShadcnAdapter(root));
239
+ assert.ok(
240
+ indicator!.classes.includes('w-[100%]'),
241
+ 'value="100" → w-[100%] (Complete state)',
242
+ );
243
+ }
244
+
245
+ // (g3) value="0" → w-[0%]
246
+ {
247
+ const root = progressTree('0');
248
+ const indicator = findIndicator(applyShadcnAdapter(root));
249
+ assert.ok(
250
+ indicator!.classes.includes('w-[0%]'),
251
+ 'value="0" → w-[0%] (Empty state)',
252
+ );
253
+ }
254
+
255
+ // (g4) value undefined / null → indeterminate 33%
256
+ {
257
+ const root = progressTree(undefined);
258
+ const indicator = findIndicator(applyShadcnAdapter(root));
259
+ assert.ok(
260
+ indicator!.classes.includes('w-[33%]'),
261
+ 'missing value → w-[33%] (Indeterminate static representation)',
262
+ );
263
+ }
264
+
265
+ // (g5) value out of range → clamped
266
+ {
267
+ const indicator250 = findIndicator(applyShadcnAdapter(progressTree('250')));
268
+ assert.ok(
269
+ indicator250!.classes.includes('w-[100%]'),
270
+ 'value="250" clamped to 100%',
271
+ );
272
+ const indicatorNeg = findIndicator(applyShadcnAdapter(progressTree('-10')));
273
+ assert.ok(
274
+ indicatorNeg!.classes.includes('w-[0%]'),
275
+ 'value="-10" clamped to 0%',
276
+ );
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Slider: defaultValue (number | number[]) → indicator + thumb positioning
281
+ // ---------------------------------------------------------------------------
282
+
283
+ function elTyped<P extends Record<string, unknown> = Record<string, string>>(
284
+ props: P,
285
+ classes: string[] = [],
286
+ children: NodeIR[] = [],
287
+ ): NodeIR {
288
+ return {
289
+ kind: 'element',
290
+ tagName: 'div',
291
+ tagLower: 'div',
292
+ props,
293
+ classes,
294
+ children,
295
+ } as unknown as NodeIR;
296
+ }
297
+
298
+ function sliderTree(rootProps: Record<string, unknown>): NodeIR {
299
+ const indicator = el({ 'data-slot': 'slider-indicator' }, ['absolute', 'h-full', 'bg-primary']);
300
+ const track = el(
301
+ { 'data-slot': 'slider-track' },
302
+ ['relative', 'h-1.5', 'w-full', 'overflow-hidden'],
303
+ [indicator],
304
+ );
305
+ const thumb = el(
306
+ { 'data-slot': 'slider-thumb' },
307
+ ['block', 'size-4', 'shrink-0', 'rounded-full'],
308
+ );
309
+ const control = el(
310
+ { 'data-slot': 'slider-control' },
311
+ ['relative', 'flex', 'h-5', 'w-full', 'grow', 'items-center'],
312
+ [track, thumb],
313
+ );
314
+ return elTyped(
315
+ Object.assign({ 'data-slot': 'slider' }, rootProps),
316
+ ['relative', 'flex', 'w-full', 'items-center'],
317
+ [control],
318
+ );
319
+ }
320
+
321
+ function findBySlot(root: NodeIR, slot: string): Array<Extract<NodeIR, { kind: 'element' }>> {
322
+ const out: Array<Extract<NodeIR, { kind: 'element' }>> = [];
323
+ const walk = (n: NodeIR) => {
324
+ const el = n as Extract<NodeIR, { kind: 'element' }>;
325
+ if (el.props && el.props['data-slot'] === slot) out.push(el);
326
+ if ('children' in el && Array.isArray(el.children)) {
327
+ for (const c of el.children) walk(c);
328
+ }
329
+ };
330
+ walk(root);
331
+ return out;
332
+ }
333
+
334
+ // (h) Single value (number, max=100) → indicator [0, 50%], one thumb @ 50%
335
+ {
336
+ const root = sliderTree({ defaultValue: 50, max: 100 });
337
+ const next = applyShadcnAdapter(root);
338
+ const ind = findBySlot(next, 'slider-indicator')[0];
339
+ assert.ok(ind.classes.includes('left-[0%]'), 'single value → indicator left-[0%]');
340
+ assert.ok(ind.classes.includes('w-[50%]'), 'single value → indicator w-[50%]');
341
+ const thumbs = findBySlot(next, 'slider-thumb');
342
+ assert.equal(thumbs.length, 1, 'single value → one thumb');
343
+ assert.ok(thumbs[0].classes.includes('left-[50%]'), 'thumb at 50%');
344
+ assert.ok(thumbs[0].classes.includes('-translate-x-1/2'), 'thumb gets centering translate');
345
+ assert.ok(thumbs[0].classes.includes('absolute'), 'thumb gets absolute');
346
+ }
347
+
348
+ // (h2) Range value [25, 75] → indicator [25%, 50% wide], two thumbs @ 25 and 75
349
+ {
350
+ const root = sliderTree({ defaultValue: [25, 75], max: 100 });
351
+ const next = applyShadcnAdapter(root);
352
+ const ind = findBySlot(next, 'slider-indicator')[0];
353
+ assert.ok(ind.classes.includes('left-[25%]'), 'range → indicator left at min%');
354
+ assert.ok(ind.classes.includes('w-[50%]'), 'range → indicator width = (max-min)%');
355
+ const thumbs = findBySlot(next, 'slider-thumb');
356
+ assert.equal(thumbs.length, 2, 'range → thumb template cloned to 2 instances');
357
+ assert.ok(thumbs[0].classes.includes('left-[25%]'), 'first thumb at min%');
358
+ assert.ok(thumbs[1].classes.includes('left-[75%]'), 'second thumb at max%');
359
+ }
360
+
361
+ // (h3) max=200 → percent scales correctly: defaultValue=100 → 50%
362
+ {
363
+ const root = sliderTree({ defaultValue: 100, max: 200 });
364
+ const next = applyShadcnAdapter(root);
365
+ const thumb = findBySlot(next, 'slider-thumb')[0];
366
+ assert.ok(thumb.classes.includes('left-[50%]'), 'value=100, max=200 → thumb at 50%');
367
+ }
368
+
369
+ // (h4) Stringified array "[25, 75]" — safety net for older scanner emit forms
370
+ {
371
+ const root = sliderTree({ defaultValue: '[25, 75]', max: 100 });
372
+ const next = applyShadcnAdapter(root);
373
+ assert.equal(findBySlot(next, 'slider-thumb').length, 2, 'stringified array also clones');
374
+ }
375
+
376
+ // (h5) No value, no defaultValue → default to single thumb @ 0%
377
+ {
378
+ const root = sliderTree({});
379
+ const next = applyShadcnAdapter(root);
380
+ const thumbs = findBySlot(next, 'slider-thumb');
381
+ assert.equal(thumbs.length, 1, 'no value → one thumb');
382
+ assert.ok(thumbs[0].classes.includes('left-[0%]'), 'no value → thumb at 0%');
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // (i) Radix ScrollArea — strip runtime control children
387
+ // ---------------------------------------------------------------------------
388
+ //
389
+ // `<ScrollAreaPrimitive.Root>` wraps a viewport + a scrollbar + thumb +
390
+ // corner. The scrollbar/thumb/corner only render at runtime in response
391
+ // to scroll position; in a static design-system render they show up as
392
+ // visible content blobs (a dark capsule over the viewport's content
393
+ // area, which is what greenhouse-app's ScrollArea story exposed). The
394
+ // adapter drops them so only the Viewport survives.
395
+
396
+ function scrollAreaTree(extraScrollbarClasses: string[] = []): NodeIR {
397
+ return el({}, [], [
398
+ {
399
+ kind: 'element',
400
+ tagName: 'ScrollAreaPrimitive.Root',
401
+ tagLower: 'div',
402
+ props: {},
403
+ classes: ['relative', 'overflow-hidden'],
404
+ children: [
405
+ {
406
+ kind: 'element',
407
+ tagName: 'ScrollAreaPrimitive.Viewport',
408
+ tagLower: 'div',
409
+ props: {},
410
+ classes: ['h-full', 'w-full'],
411
+ children: [el({}, ['p-3'], [])],
412
+ },
413
+ {
414
+ kind: 'element',
415
+ tagName: 'ScrollAreaPrimitive.Scrollbar',
416
+ tagLower: 'div',
417
+ props: { orientation: 'vertical' },
418
+ classes: ['flex', 'p-0.5', 'sm:bg-black/60', ...extraScrollbarClasses],
419
+ children: [{
420
+ kind: 'element',
421
+ tagName: 'ScrollAreaPrimitive.Thumb',
422
+ tagLower: 'div',
423
+ props: {},
424
+ classes: ['flex-1', 'rounded-full', 'bg-muted-foreground/40'],
425
+ children: [],
426
+ }],
427
+ },
428
+ {
429
+ kind: 'element',
430
+ tagName: 'ScrollAreaPrimitive.Corner',
431
+ tagLower: 'div',
432
+ props: {},
433
+ classes: [],
434
+ children: [],
435
+ },
436
+ ],
437
+ } as unknown as NodeIR,
438
+ ]);
439
+ }
440
+
441
+ // (i1) Scrollbar + Thumb + Corner are removed; Viewport is flattened
442
+ // (its children become Root's direct children).
443
+ {
444
+ const tree = scrollAreaTree();
445
+ const next = applyShadcnAdapter(tree);
446
+ const root = (next as { children: NodeIR[] }).children[0] as { children: NodeIR[] };
447
+ assert.equal(
448
+ root.children.length,
449
+ 1,
450
+ 'Root should have one child after Viewport flatten (the Viewport content was a single styled div)',
451
+ );
452
+ // The surviving child is the *content of* the Viewport, not the
453
+ // Viewport wrapper itself. The Viewport carried no visual styling
454
+ // worth keeping and Figma was painting it as a ghost-bordered
455
+ // duplicate of Root.
456
+ assert.notEqual(
457
+ (root.children[0] as { tagName?: string }).tagName,
458
+ 'ScrollAreaPrimitive.Viewport',
459
+ 'Viewport wrapper must be flattened — its children move up to Root',
460
+ );
461
+ assert.equal(
462
+ (root.children[0] as { tagName?: string }).tagName,
463
+ 'div',
464
+ 'the surviving child is the original Viewport content (a styled <div>)',
465
+ );
466
+ }
467
+
468
+ // (i2) Viewport content's own children are preserved unchanged.
469
+ {
470
+ const tree = scrollAreaTree();
471
+ const next = applyShadcnAdapter(tree);
472
+ const root = (next as { children: NodeIR[] }).children[0] as { children: { classes: string[] }[] };
473
+ assert.deepEqual(
474
+ root.children[0].classes,
475
+ ['p-3'],
476
+ "the inner content's class list should be preserved verbatim after flatten",
477
+ );
478
+ }
479
+
480
+ console.log('framework-adapter-shadcn-regression: PASS (3 registry + 4 leaf + 4 avatar + 5 progress + 5 slider + 2 scroll-area cases)');