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,198 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ COMPONENT_SECTION_ORDER,
4
+ getComponentSectionName,
5
+ groupComponentDefs,
6
+ inferComponentSection,
7
+ } from '../src/design-system/component-sections';
8
+ import type { ComponentDef } from '../src/components';
9
+
10
+ /**
11
+ * Regression: component-section classification + grouping is the routing
12
+ * logic that decides which heading each component lands under in the
13
+ * generated Figma Design System page. It runs on every preflight + render,
14
+ * so silent drift here scrambles the page layout for every consumer.
15
+ *
16
+ * The rules are a cascade — explicit `kind` short-circuits everything,
17
+ * then heuristics on `type`, `isLeaf`, `usesCount`, `usedByCount`,
18
+ * `hasStory` decide the bucket. Lock the cascade in so refactors don't
19
+ * silently flip atoms into molecules or vice versa.
20
+ *
21
+ * Extracted from `src/design-system/story-builder.ts` (was the named refactor
22
+ * target in `.ai/index.md`) into `src/design-system/component-sections.ts`.
23
+ */
24
+
25
+ function def(overrides: Partial<ComponentDef> & { name: string }): ComponentDef {
26
+ return {
27
+ type: 'simple',
28
+ stories: [],
29
+ ...overrides,
30
+ } as ComponentDef;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Section order + section name format
35
+ // ---------------------------------------------------------------------------
36
+ {
37
+ const keys = COMPONENT_SECTION_ORDER.map((s) => s.key);
38
+ assert.deepEqual(
39
+ keys,
40
+ ['atoms', 'molecules', 'organisms', 'utilities', 'other'],
41
+ 'COMPONENT_SECTION_ORDER drifted — atomic ordering atoms→molecules→organisms→utilities→other is canonical',
42
+ );
43
+
44
+ const atom = COMPONENT_SECTION_ORDER[0];
45
+ assert.equal(
46
+ getComponentSectionName(atom),
47
+ 'Section / Atoms',
48
+ 'getComponentSectionName must format with the `Section / ` prefix — that prefix is what the page-builder finds frames by',
49
+ );
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Explicit `kind` short-circuits the heuristic
54
+ // ---------------------------------------------------------------------------
55
+ {
56
+ const cases: Array<{ kind: string; expectedKey: string }> = [
57
+ { kind: 'atom', expectedKey: 'atoms' },
58
+ { kind: 'molecule', expectedKey: 'molecules' },
59
+ { kind: 'organism', expectedKey: 'organisms' },
60
+ { kind: 'utility', expectedKey: 'utilities' },
61
+ { kind: 'other', expectedKey: 'other' },
62
+ { kind: 'ATOM', expectedKey: 'atoms' }, // case-insensitive
63
+ ];
64
+ for (const { kind, expectedKey } of cases) {
65
+ // Stack the heuristic against the kind to prove kind wins. usesCount=5 +
66
+ // usedByCount=0 + type='compound' would otherwise route to organisms.
67
+ const d = def({
68
+ name: 'X',
69
+ kind,
70
+ type: 'compound',
71
+ usesCount: 5,
72
+ usedByCount: 0,
73
+ });
74
+ assert.equal(
75
+ inferComponentSection(d).key,
76
+ expectedKey,
77
+ `explicit kind="${kind}" must short-circuit to ${expectedKey}`,
78
+ );
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Fallback heuristic cascade (no kind)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ // !hasStory + usedByCount=0 + isLeaf → utilities (orphan leaf with no story)
87
+ assert.equal(
88
+ inferComponentSection(
89
+ def({ name: 'OrphanLeaf', hasStory: false, isLeaf: true, usedByCount: 0 }),
90
+ ).key,
91
+ 'utilities',
92
+ 'leaf without story or consumers must land in utilities (cn(), helpers, etc.)',
93
+ );
94
+
95
+ // type='cva' → atoms (regardless of leaf/usedBy)
96
+ assert.equal(
97
+ inferComponentSection(def({ name: 'Button', type: 'cva', usesCount: 0, usedByCount: 8 })).key,
98
+ 'atoms',
99
+ 'CVA components are always atoms — they own variant matrices',
100
+ );
101
+
102
+ // type='state' → atoms (input, switch, checkbox)
103
+ assert.equal(
104
+ inferComponentSection(def({ name: 'Input', type: 'state', isLeaf: true })).key,
105
+ 'atoms',
106
+ 'state-based components are always atoms — they own hover/focus/disabled matrices',
107
+ );
108
+
109
+ // isLeaf + usedByCount>0 → atoms (leaf used somewhere = primitive)
110
+ assert.equal(
111
+ inferComponentSection(def({ name: 'Icon', isLeaf: true, usedByCount: 4 })).key,
112
+ 'atoms',
113
+ 'a leaf used by others is a primitive — atom',
114
+ );
115
+
116
+ // usesCount>=2 → organisms (composes multiple subcomponents)
117
+ assert.equal(
118
+ inferComponentSection(def({ name: 'PageHeader', usesCount: 3, usedByCount: 0 })).key,
119
+ 'organisms',
120
+ 'component composing 2+ subcomponents is an organism',
121
+ );
122
+
123
+ // type='compound' → molecules
124
+ assert.equal(
125
+ inferComponentSection(def({ name: 'Card', type: 'compound', usesCount: 1 })).key,
126
+ 'molecules',
127
+ 'compound components (one slot/sub-component) are molecules',
128
+ );
129
+
130
+ // usesCount=1 → molecules
131
+ assert.equal(
132
+ inferComponentSection(def({ name: 'IconButton', usesCount: 1 })).key,
133
+ 'molecules',
134
+ 'component using exactly one subcomponent is a molecule',
135
+ );
136
+
137
+ // usedByCount>0 (with usesCount=0, no kind, no special type) → molecules
138
+ assert.equal(
139
+ inferComponentSection(def({ name: 'UsedThing', usesCount: 0, usedByCount: 2, isLeaf: false })).key,
140
+ 'molecules',
141
+ 'component used by others but not a leaf falls into molecules',
142
+ );
143
+
144
+ // isLeaf with no other signals → atoms
145
+ assert.equal(
146
+ inferComponentSection(def({ name: 'BareLeaf', isLeaf: true, usedByCount: 0 })).key,
147
+ 'atoms',
148
+ 'a bare leaf with no other signal is an atom',
149
+ );
150
+
151
+ // Nothing matches → other
152
+ assert.equal(
153
+ inferComponentSection(def({ name: 'Mystery', isLeaf: false, usesCount: 0, usedByCount: 0 })).key,
154
+ 'other',
155
+ 'a non-leaf with no uses and no consumers and no kind/type defaults to other',
156
+ );
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // groupComponentDefs: ordering, omission of empty sections, alphabetical sort
160
+ // ---------------------------------------------------------------------------
161
+ {
162
+ const defs: ComponentDef[] = [
163
+ def({ name: 'Zebra', kind: 'atom' }),
164
+ def({ name: 'Antelope', kind: 'atom' }),
165
+ def({ name: 'Lemur', kind: 'molecule' }),
166
+ def({ name: 'Bison', kind: 'organism' }),
167
+ ];
168
+
169
+ const grouped = groupComponentDefs(defs);
170
+
171
+ // Canonical order — atoms before molecules before organisms; utilities + other absent because empty.
172
+ assert.deepEqual(
173
+ grouped.map((g) => g.section.key),
174
+ ['atoms', 'molecules', 'organisms'],
175
+ 'groupComponentDefs must return sections in canonical order, omitting empty ones',
176
+ );
177
+
178
+ // Within atoms: alphabetical sort (Antelope before Zebra).
179
+ assert.deepEqual(
180
+ grouped[0].defs.map((d: ComponentDef) => d.name),
181
+ ['Antelope', 'Zebra'],
182
+ 'defs within a section must be sorted alphabetically by name',
183
+ );
184
+
185
+ // Single-entry sections have one item.
186
+ assert.equal(grouped[1].defs.length, 1);
187
+ assert.equal(grouped[1].defs[0].name, 'Lemur');
188
+ assert.equal(grouped[2].defs[0].name, 'Bison');
189
+ }
190
+
191
+ // Empty input → empty result (no sections rendered).
192
+ assert.deepEqual(
193
+ groupComponentDefs([]),
194
+ [],
195
+ 'empty input must produce empty grouping (no headers for sections without content)',
196
+ );
197
+
198
+ console.log('component-sections-regression: PASS');
@@ -0,0 +1,163 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import { getCompoundClasses } from '../src/tailwind/class-utils';
4
+ import type { ComponentDef } from '../src/components/scanner-types';
5
+
6
+ /**
7
+ * Regression: `getCompoundClasses(def, tagName)` uses a STRICT lowercase
8
+ * compare — `<DialogPrimitive.Trigger>` must NOT match a subComponent
9
+ * registered as `DialogTrigger`.
10
+ *
11
+ * History: an earlier attempt used the same elaborate normalization as
12
+ * `getComponentDefByName` so that `<AvatarPrimitive.Image>` would
13
+ * resolve to the `AvatarImage` subComponent and get flattened to a div
14
+ * (so the renderer's image branch could pick up its `src` prop). That
15
+ * change shipped, fixed Avatar's image render, and silently broke every
16
+ * portal-using component (Dialog / Sheet / Select / Accordion-open /
17
+ * Mobile-Nav) — their portal primitives (`<DialogPrimitive.Trigger>`,
18
+ * `<SheetPrimitive.Content>`, ...) suddenly matched a subComponent,
19
+ * got flattened to `kind: 'element'`, and were routed through the
20
+ * wrapper-frame path instead of the kind-'component' path that knows
21
+ * how to position portals as overlays. The user saw trigger + gray
22
+ * content rendered side-by-side instead of trigger + portal panel.
23
+ *
24
+ * AvatarPrimitive.Image is unaffected by the strict match because the
25
+ * image branch in ui-builder (`isLeafImageLike`) accepts BOTH
26
+ * `kind: 'element'` and `kind: 'component'` and gates only on the
27
+ * presence of a `src` prop. The primitive stays `kind: 'component'`
28
+ * after the strict lookup but still renders correctly.
29
+ *
30
+ * This file locks the strict-match behavior so a future "let's
31
+ * normalize this lookup" doesn't re-break portals.
32
+ */
33
+
34
+ function compoundDef(): ComponentDef {
35
+ return {
36
+ type: 'compound',
37
+ name: 'Avatar',
38
+ subComponents: [
39
+ { name: 'Avatar', slot: 'container', classes: ['relative', 'flex', 'size-10'] },
40
+ { name: 'AvatarImage', slot: 'container', classes: ['aspect-square', 'size-full'] },
41
+ { name: 'AvatarFallback', slot: 'container', classes: ['flex', 'size-full', 'bg-muted'] },
42
+ ],
43
+ } as unknown as ComponentDef;
44
+ }
45
+
46
+ function dialogDef(): ComponentDef {
47
+ return {
48
+ type: 'compound',
49
+ name: 'Dialog',
50
+ subComponents: [
51
+ { name: 'Dialog', slot: 'container', classes: [] },
52
+ { name: 'DialogTrigger', slot: 'container', classes: [] },
53
+ { name: 'DialogContent', slot: 'container', classes: ['fixed', 'top-1/2', 'left-1/2'] },
54
+ { name: 'DialogHeader', slot: 'container', classes: [] },
55
+ ],
56
+ } as unknown as ComponentDef;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Exact-name matches: the everyday case still works.
61
+ // ---------------------------------------------------------------------------
62
+
63
+ {
64
+ const def = compoundDef();
65
+ assert.deepEqual(
66
+ getCompoundClasses(def, 'AvatarImage'),
67
+ ['aspect-square', 'size-full'],
68
+ 'exact-name match: AvatarImage → its registered classes',
69
+ );
70
+ assert.deepEqual(
71
+ getCompoundClasses(def, 'AvatarFallback'),
72
+ ['flex', 'size-full', 'bg-muted'],
73
+ 'exact-name match: AvatarFallback → its registered classes',
74
+ );
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Portal-primitive tags MUST NOT match by normalization.
79
+ // `<DialogPrimitive.Trigger>` and friends must stay `kind: 'component'`
80
+ // so the portal-aware render path positions them as overlays. If a
81
+ // future change re-introduces normalization here, this assertion fires
82
+ // and the matrix of portal regressions surfaces at test time instead
83
+ // of "trigger + gray bar in every Dialog story".
84
+ // ---------------------------------------------------------------------------
85
+
86
+ {
87
+ const def = dialogDef();
88
+ assert.deepEqual(
89
+ getCompoundClasses(def, 'DialogPrimitive.Trigger'),
90
+ [],
91
+ 'DialogPrimitive.Trigger must NOT match DialogTrigger via normalization',
92
+ );
93
+ assert.deepEqual(
94
+ getCompoundClasses(def, 'DialogPrimitive.Content'),
95
+ [],
96
+ 'DialogPrimitive.Content must NOT match DialogContent via normalization',
97
+ );
98
+ assert.deepEqual(
99
+ getCompoundClasses(def, 'DialogPrimitive.Root'),
100
+ [],
101
+ 'DialogPrimitive.Root must NOT match Dialog via normalization',
102
+ );
103
+ }
104
+
105
+ {
106
+ const def = compoundDef();
107
+ // Same rule for Avatar's primitive form — strict miss is the desired
108
+ // behavior here, the image branch will pick up the src prop without
109
+ // needing the subComponent classes merged in.
110
+ assert.deepEqual(
111
+ getCompoundClasses(def, 'AvatarPrimitive.Image'),
112
+ [],
113
+ 'AvatarPrimitive.Image strictly misses — image branch handles it via src prop',
114
+ );
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Root-fallback: the def's own name resolves to the container subComponent.
119
+ // ---------------------------------------------------------------------------
120
+
121
+ {
122
+ const def = compoundDef();
123
+ assert.deepEqual(
124
+ getCompoundClasses(def, 'Avatar'),
125
+ ['relative', 'flex', 'size-10'],
126
+ 'the root name resolves to the container subComponent (first-match returns AvatarImage’s `slot: container` — guarded by exact-name match above)',
127
+ );
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Negative cases — unrelated tags don't match.
132
+ // ---------------------------------------------------------------------------
133
+
134
+ {
135
+ const def = compoundDef();
136
+ assert.deepEqual(
137
+ getCompoundClasses(def, 'SomeOtherImage'),
138
+ [],
139
+ 'a tag that shares only a suffix must not match',
140
+ );
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Defensive cases — empty / non-compound defs.
145
+ // ---------------------------------------------------------------------------
146
+
147
+ assert.deepEqual(
148
+ getCompoundClasses(null as unknown as ComponentDef, 'X'),
149
+ [],
150
+ 'null def → []',
151
+ );
152
+ assert.deepEqual(
153
+ getCompoundClasses({ type: 'cva', name: 'Button' } as unknown as ComponentDef, 'Button'),
154
+ [],
155
+ 'non-compound def → [] (this helper is compound-only)',
156
+ );
157
+ assert.deepEqual(
158
+ getCompoundClasses({ type: 'compound', name: 'X', subComponents: [] } as unknown as ComponentDef, 'X'),
159
+ [],
160
+ 'empty subComponents → []',
161
+ );
162
+
163
+ console.log('compound-classes-lookup-regression: PASS (2 exact + 4 portal-miss + 1 root + 1 negative + 3 defensive cases)');
@@ -14,16 +14,17 @@ function testRootAndThemeSelectors(): void {
14
14
  .dark {
15
15
  --primary: oklch(70% 0.14 150);
16
16
  }
17
- :root[data-theme="secondary"] {
17
+ .secondary {
18
18
  --primary: oklch(62% 0.17 250);
19
19
  }
20
20
  `;
21
- const map = parseCssTokenMap(css, 'src/app/globals.css', 'auto');
21
+ const map = parseCssTokenMap(css, 'src/app/globals.css', 'css' as const);
22
22
  assert.equal(map.mode, 'css');
23
23
  assert.equal(map.colors.primary, 'oklch(62% 0.17 149)');
24
24
  assert.equal(map.radius.base, 8);
25
25
  assert.equal(map.fonts.sans, '"Open Sans", sans-serif');
26
- assert.equal(map.themes.dark?.colors.primary, 'oklch(70% 0.14 150)');
26
+ // .dark {} is a dark-mode override, not a brand theme — should not appear
27
+ assert.equal(map.themes.dark, undefined);
27
28
  assert.equal(map.themes.secondary?.colors.primary, 'oklch(62% 0.17 250)');
28
29
  }
29
30
 
@@ -35,7 +36,7 @@ function testTailwindV4ThemeBlock(): void {
35
36
  --text-xl: 1.25rem;
36
37
  }
37
38
  `;
38
- const map = parseCssTokenMap(css, 'src/app/globals.css', 'auto');
39
+ const map = parseCssTokenMap(css, 'src/app/globals.css', 'css' as const);
39
40
  assert.equal(map.colors.primary, 'oklch(0.7 0.2 150)');
40
41
  assert.equal(map.spacing.md, 16);
41
42
  assert.equal(map.fontSize.xl, 20);
@@ -49,7 +50,7 @@ function testShadowAndColorPrefixes(): void {
49
50
  --color-background: oklch(1 0 0);
50
51
  }
51
52
  `;
52
- const map = parseCssTokenMap(css, 'src/app/tokens.css', 'auto');
53
+ const map = parseCssTokenMap(css, 'src/app/tokens.css', 'css' as const);
53
54
  assert.equal(map.shadows.DEFAULT, '0 1px 2px rgba(0,0,0,.1)');
54
55
  assert.equal(map.shadows.md, '0 8px 16px rgba(0,0,0,.2)');
55
56
  assert.equal(map.colors.background, 'oklch(1 0 0)');
@@ -74,7 +75,7 @@ function testImportedCssSourceResolution(): void {
74
75
  'utf8'
75
76
  );
76
77
 
77
- const map = readTokenSourceMap({ projectRoot: tmpRoot, tokenSourceMode: 'auto' });
78
+ const map = readTokenSourceMap({ projectRoot: tmpRoot, tokenSourceMode: 'css' as const });
78
79
  assert.equal(map.mode, 'css');
79
80
  assert.equal(map.source.replace(/\\/g, '/'), 'src/theme/brand.css');
80
81
  assert.equal(map.colors.primary, 'oklch(0.62 0.19 250)');