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,5 @@
1
+ export * from './blob-placement';
2
+ export * from './clip-path-decorative';
3
+ export * from './radial-gradient';
4
+ export * from './icon-builder';
5
+ export * from './portal-panel';
@@ -0,0 +1,369 @@
1
+ /**
2
+ * portal-panel.ts — pre-pass that injects explicit widths on portal panels.
3
+ *
4
+ * Portal-rendered components (Select dropdown, DropdownMenu content, Popover,
5
+ * Tooltip) visually escape their parent in the browser. The Figma plugin can't
6
+ * portal, so we render them in-place under the story layout. The challenge:
7
+ * panels without an explicit `max-w-*` class (Radix popper sizes to content
8
+ * with a `min-w-[8rem]` floor) have no width context at render time — their
9
+ * `w-full` children collapse in HUG parents and item text wraps aggressively.
10
+ *
11
+ * Solution: a pre-pass that walks the NodeIR tree and, for `__fromPortal` nodes
12
+ * without an explicit width, measures the intrinsic content width (longest
13
+ * text path + cumulative padding) and injects a synthetic `w-[Npx]` class.
14
+ * The standard rendering pipeline then treats the panel as any fixed-width
15
+ * frame — no shared-code branches or deferred apply logic required.
16
+ *
17
+ * Text widths come from real Figma text nodes (same path as the renderer), so
18
+ * the injected width matches rendered width. Each probe text node is created,
19
+ * read, and removed immediately — no lasting effect on the document.
20
+ */
21
+
22
+ import { isElementLikeNode, type NodeIR, type NodeIRElementBase } from '../tailwind';
23
+ import { createTextNode } from '../text';
24
+
25
+ const PANEL_WIDTH_BUFFER_PX = 4;
26
+ const DEFAULT_PANEL_GAP_CLASS = 'mt-1';
27
+
28
+ const FONT_SIZE_PX: Record<string, number> = {
29
+ 'text-xs': 12,
30
+ 'text-sm': 14,
31
+ 'text-base': 16,
32
+ 'text-lg': 18,
33
+ 'text-xl': 20,
34
+ 'text-2xl': 24,
35
+ };
36
+
37
+ export function injectPortalPanelWidths(tree: NodeIR): NodeIR {
38
+ return walk(tree, 14);
39
+ }
40
+
41
+ const DRAWER_PANEL_HEIGHT_PX = 700;
42
+ const DRAWER_PANEL_WIDTH_PX = 360;
43
+
44
+ /**
45
+ * Portal panels with `h-full` (drawer/sheet content) need a fixed height so
46
+ * descendant `flex-1` siblings have vertical space to claim. They also need a
47
+ * concrete pixel width because their real parent in the browser is the
48
+ * viewport — in Figma auto-layout the nearest ancestor is a HUG-to-content
49
+ * container (Sheet.Root) that collapses to the sibling trigger's width.
50
+ *
51
+ * Inject both `h-[700px]` and `w-[360px]` so the drawer renders at drawer
52
+ * dimensions regardless of its wrapper chain.
53
+ */
54
+ export function injectPortalPanelHeights(tree: NodeIR): NodeIR {
55
+ return walkHeights(tree);
56
+ }
57
+
58
+ function walkHeights(node: NodeIR): NodeIR {
59
+ if (node.kind === 'text' || node.kind === 'divider') return node;
60
+ if (node.kind === 'fragment') {
61
+ const nextChildren = node.children.map(walkHeights);
62
+ return nextChildren.some((c, i) => c !== node.children[i])
63
+ ? { ...node, children: nextChildren }
64
+ : node;
65
+ }
66
+ if (node.kind === 'ring') {
67
+ const nextChild = walkHeights(node.child);
68
+ return nextChild !== node.child ? { ...node, child: nextChild } : node;
69
+ }
70
+
71
+ const nextChildren = node.children.map(walkHeights);
72
+ let next: NodeIRElementBase = node;
73
+ if (nextChildren.some((c, i) => c !== node.children[i])) {
74
+ next = { ...node, children: nextChildren } as NodeIRElementBase;
75
+ }
76
+
77
+ const isPortalPanel = !!(next.props && next.props.__fromPortal);
78
+ if (!isPortalPanel) return next as NodeIR;
79
+
80
+ const hasHFull = next.classes.indexOf('h-full') !== -1
81
+ || next.classes.indexOf('h-screen') !== -1
82
+ || next.classes.indexOf('min-h-screen') !== -1;
83
+ if (!hasHFull) return next as NodeIR;
84
+
85
+ if (hasExplicitPanelHeight(next.classes)) return next as NodeIR;
86
+
87
+ const hasParentRelativeWidth = classListHasParentRelativeWidth(next.classes);
88
+ const shouldInjectWidth = hasParentRelativeWidth && !classListHasFixedWidth(next.classes);
89
+
90
+ const withFixed: string[] = [];
91
+ for (let i = 0; i < next.classes.length; i++) {
92
+ const cls = next.classes[i];
93
+ // Strip generic full-parent sizing sentinels that can't resolve without a
94
+ // fixed viewport-sized ancestor — we inject concrete pixel values below.
95
+ if (cls === 'h-full' || cls === 'h-screen' || cls === 'min-h-screen') continue;
96
+ if (shouldInjectWidth && (cls === 'w-full' || /^w-\d+\/\d+$/.test(cls))) continue;
97
+ withFixed.push(cls);
98
+ }
99
+ withFixed.push(`h-[${DRAWER_PANEL_HEIGHT_PX}px]`);
100
+ if (shouldInjectWidth) withFixed.push(`w-[${DRAWER_PANEL_WIDTH_PX}px]`);
101
+ return { ...next, classes: withFixed } as NodeIR;
102
+ }
103
+
104
+ function classListHasParentRelativeWidth(classes: string[]): boolean {
105
+ for (let i = 0; i < classes.length; i++) {
106
+ const cls = classes[i];
107
+ if (cls === 'w-full') return true;
108
+ if (/^w-\d+\/\d+$/.test(cls)) return true;
109
+ }
110
+ return false;
111
+ }
112
+
113
+ function classListHasFixedWidth(classes: string[]): boolean {
114
+ for (let i = 0; i < classes.length; i++) {
115
+ const cls = classes[i];
116
+ if (/^w-\[/.test(cls)) return true;
117
+ if (/^w-\d+$/.test(cls)) return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function hasExplicitPanelHeight(classes: string[]): boolean {
123
+ for (let i = 0; i < classes.length; i++) {
124
+ const cls = classes[i];
125
+ if (/^h-\[/.test(cls)) return true;
126
+ if (/^h-\d/.test(cls)) return true;
127
+ if (/^max-h-/.test(cls)) return true;
128
+ }
129
+ return false;
130
+ }
131
+
132
+ function walk(node: NodeIR, parentFontSize: number): NodeIR {
133
+ if (node.kind === 'text' || node.kind === 'divider') return node;
134
+ if (node.kind === 'fragment') {
135
+ const nextChildren = node.children.map((c) => walk(c, parentFontSize));
136
+ return { ...node, children: nextChildren };
137
+ }
138
+ if (node.kind === 'ring') {
139
+ return { ...node, child: walk(node.child, parentFontSize) };
140
+ }
141
+
142
+ const fs = resolveFontSize(node.classes, parentFontSize);
143
+ // Recurse first so children are processed (though we don't currently use
144
+ // their injected widths when measuring parent — raw NodeIR classes drive
145
+ // the estimate).
146
+ const nextChildren = node.children.map((c) => walk(c, fs));
147
+ let next: NodeIRElementBase = node;
148
+ if (nextChildren.some((c, i) => c !== node.children[i])) {
149
+ next = { ...node, children: nextChildren } as NodeIRElementBase;
150
+ }
151
+
152
+ const isPortalPanel = !!(next.props && next.props.__fromPortal);
153
+ if (!isPortalPanel) return next as NodeIR;
154
+
155
+ // Drawer/sheet-style panels (`h-full` + `w-full` / fractional width) are
156
+ // viewport-sized, not content-sized. The heights pre-pass handles them and
157
+ // injects concrete viewport pixels. Running intrinsic-width measurement here
158
+ // would inject a too-narrow `w-[Npx]` that the heights pass then treats as
159
+ // an explicit width and skips its own injection.
160
+ const hasFullHeight = next.classes.indexOf('h-full') !== -1
161
+ || next.classes.indexOf('h-screen') !== -1
162
+ || next.classes.indexOf('min-h-screen') !== -1;
163
+ if (hasFullHeight && classListHasParentRelativeWidth(next.classes)) {
164
+ return next as NodeIR;
165
+ }
166
+
167
+ let nextClasses: string[] | null = null;
168
+
169
+ // Radix popper panels use `translate-y-1` to create a visual gap from the
170
+ // trigger. CSS transforms have no effect in Figma auto-layout — translate
171
+ // any present `translate-y-*` to `mt-*`, and fall back to `mt-1` when the
172
+ // scanner didn't collect a translate class. The plugin converts child
173
+ // `mt-*` into parent itemSpacing, which renders as the trigger→panel gap.
174
+ const mtRewritten = rewriteTranslateYToMarginTop(next.classes);
175
+ if (mtRewritten !== next.classes) {
176
+ nextClasses = mtRewritten;
177
+ } else if (!hasExplicitMarginTop(next.classes)) {
178
+ nextClasses = next.classes.concat([DEFAULT_PANEL_GAP_CLASS]);
179
+ }
180
+
181
+ const baseClasses = nextClasses || next.classes;
182
+ if (!hasExplicitPanelWidth(baseClasses)) {
183
+ const measured = measurePanelWidth({ ...next, classes: baseClasses }, fs);
184
+ if (Number.isFinite(measured) && measured > 0) {
185
+ // Small right-edge buffer: Figma text measurement can round down and
186
+ // the last item in a HUG container otherwise sits flush against the
187
+ // panel border.
188
+ const widthPx = Math.round(measured) + PANEL_WIDTH_BUFFER_PX;
189
+ nextClasses = baseClasses.concat([`w-[${widthPx}px]`]);
190
+ }
191
+ }
192
+
193
+ if (!nextClasses) return next as NodeIR;
194
+ return { ...next, classes: nextClasses } as NodeIR;
195
+ }
196
+
197
+ function hasExplicitMarginTop(classes: string[]): boolean {
198
+ for (const cls of classes) {
199
+ if (/^mt-/.test(cls)) return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ function rewriteTranslateYToMarginTop(classes: string[]): string[] {
205
+ let rewritten: string[] | null = null;
206
+ for (let i = 0; i < classes.length; i++) {
207
+ const cls = classes[i];
208
+ const m = cls.match(/^translate-y-(.+)$/);
209
+ if (!m) continue;
210
+ if (!rewritten) rewritten = classes.slice();
211
+ rewritten[i] = 'mt-' + m[1];
212
+ }
213
+ return rewritten || classes;
214
+ }
215
+
216
+ function hasExplicitPanelWidth(classes: string[]): boolean {
217
+ for (const cls of classes) {
218
+ if (cls === 'w-full' || cls === 'w-auto') continue;
219
+ if (/^w-/.test(cls)) return true;
220
+ if (/^max-w-/.test(cls) && !/^max-w-\[(?:calc|min|max)\(/.test(cls)) return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ function resolveFontSize(classes: string[] | undefined, fallback: number): number {
226
+ if (!classes) return fallback;
227
+ for (const cls of classes) {
228
+ const px = FONT_SIZE_PX[cls];
229
+ if (px != null) return px;
230
+ }
231
+ return fallback;
232
+ }
233
+
234
+ function parsePx(token: string): number {
235
+ const bracket = token.match(/^\[(.+)\]$/);
236
+ if (bracket) {
237
+ const inner = bracket[1];
238
+ if (inner.endsWith('rem')) return parseFloat(inner) * 16;
239
+ if (inner.endsWith('px')) return parseFloat(inner);
240
+ return 0;
241
+ }
242
+ const n = parseFloat(token);
243
+ return Number.isFinite(n) ? n * 4 : 0;
244
+ }
245
+
246
+ function paddingH(classes: string[]): number {
247
+ let left = 0, right = 0;
248
+ for (const cls of classes) {
249
+ if (cls.startsWith('px-')) {
250
+ const v = parsePx(cls.slice(3));
251
+ left = v; right = v;
252
+ } else if (cls.startsWith('pl-')) {
253
+ left = parsePx(cls.slice(3));
254
+ } else if (cls.startsWith('pr-')) {
255
+ right = parsePx(cls.slice(3));
256
+ } else if (/^p-[\d\[]/.test(cls)) {
257
+ const v = parsePx(cls.slice(2));
258
+ left = v; right = v;
259
+ }
260
+ }
261
+ return left + right;
262
+ }
263
+
264
+ function gapX(classes: string[]): number {
265
+ for (const cls of classes) {
266
+ if (cls.startsWith('gap-x-')) return parsePx(cls.slice(6));
267
+ if (cls.startsWith('gap-')) return parsePx(cls.slice(4));
268
+ }
269
+ return 0;
270
+ }
271
+
272
+ function minPanelWidth(classes: string[]): number | null {
273
+ for (const cls of classes) {
274
+ if (cls.startsWith('min-w-')) {
275
+ const v = parsePx(cls.slice(6));
276
+ if (v > 0) return v;
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+
282
+ function explicitFixedWidth(classes: string[]): number | null {
283
+ for (const cls of classes) {
284
+ if (cls === 'w-full' || cls === 'w-auto') continue;
285
+ if (/^w-/.test(cls)) {
286
+ const v = parsePx(cls.slice(2));
287
+ if (v > 0) return v;
288
+ }
289
+ if (cls.startsWith('size-')) {
290
+ const v = parsePx(cls.slice(5));
291
+ if (v > 0) return v;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+
297
+ /**
298
+ * Measure the intrinsic content width of a node subtree by walking the NodeIR
299
+ * and summing/maxing child widths + layout padding/gap at each level. Text
300
+ * widths come from real Figma text nodes (created and removed per probe).
301
+ */
302
+ function measurePanelWidth(node: NodeIRElementBase, parentFontSize: number): number {
303
+ const measured = measure(node, parentFontSize);
304
+ const minW = minPanelWidth(node.classes) ?? 0;
305
+ return Math.max(measured, minW);
306
+ }
307
+
308
+ function measure(node: NodeIR, parentFontSize: number): number {
309
+ if (node.kind === 'text') {
310
+ return measureTextWidth(node.text, parentFontSize);
311
+ }
312
+ if (node.kind === 'fragment') {
313
+ let max = 0;
314
+ for (const c of node.children) {
315
+ const w = measure(c, parentFontSize);
316
+ if (w > max) max = w;
317
+ }
318
+ return max;
319
+ }
320
+ if (node.kind === 'divider') return 0;
321
+ if (node.kind === 'ring') return measure(node.child, parentFontSize);
322
+ if (!isElementLikeNode(node)) return 0;
323
+
324
+ const classes = (node as NodeIRElementBase).classes || [];
325
+ // Out-of-flow siblings don't contribute to parent's in-flow width.
326
+ if (classes.includes('absolute') || classes.includes('fixed')) return 0;
327
+
328
+ const fixed = explicitFixedWidth(classes);
329
+ if (fixed != null) return fixed;
330
+
331
+ const fs = resolveFontSize(classes, parentFontSize);
332
+ const isHorizontal = classes.includes('flex') && !classes.includes('flex-col');
333
+ const gap = isHorizontal ? gapX(classes) : 0;
334
+ const pad = paddingH(classes);
335
+
336
+ const children = (node as NodeIRElementBase).children || [];
337
+ const widths: number[] = [];
338
+ for (const c of children) {
339
+ const w = measure(c, fs);
340
+ if (w > 0) widths.push(w);
341
+ }
342
+
343
+ let content = 0;
344
+ if (isHorizontal && widths.length > 0) {
345
+ content = widths.reduce((a, b) => a + b, 0) + gap * Math.max(0, widths.length - 1);
346
+ } else if (widths.length > 0) {
347
+ let maxW = widths[0];
348
+ for (let i = 1; i < widths.length; i++) {
349
+ if (widths[i] > maxW) maxW = widths[i];
350
+ }
351
+ content = maxW;
352
+ }
353
+
354
+ return content + pad;
355
+ }
356
+
357
+ function measureTextWidth(text: string, fontSize: number): number {
358
+ if (!text) return 0;
359
+ try {
360
+ const t = createTextNode(text, { fontSize });
361
+ const w = t.width;
362
+ try { t.remove(); } catch { /* ignore */ }
363
+ return w;
364
+ } catch {
365
+ // Fallback to char-width heuristic if Figma text-node creation fails
366
+ // (e.g. font not available).
367
+ return Math.ceil(text.length * fontSize * 0.55);
368
+ }
369
+ }
@@ -1,4 +1,4 @@
1
- import type { Transform2x3 } from './transform-math';
1
+ import type { Transform2x3 } from '../tailwind';
2
2
 
3
3
  export type RadialAnchor = {
4
4
  x: number;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Framework adapters — translate UI-framework-specific runtime conventions
3
+ * (CSS rules, JS-driven layout) into class-list patches the plugin's
4
+ * Tailwind pipeline can see.
5
+ *
6
+ * The plugin scans className strings only. Frameworks that move layout
7
+ * intent OUT of className strings (shadcn's `[data-slot="..."]`
8
+ * stylesheet rules, MUI's emotion-injected styles, ...) need a small
9
+ * translation layer so the renderer doesn't have to learn each
10
+ * framework's idioms. That layer lives here.
11
+ *
12
+ * Each adapter is one file, pure, and idempotent. The dispatcher
13
+ * (`applyFrameworkAdapters`) composes them in order. Adding a new
14
+ * framework is a new file plus one line below.
15
+ */
16
+
17
+ import type { NodeIR } from '../tailwind/node-ir';
18
+ import { applyShadcnAdapter, isShadcnAdapterDroppedTag } from './shadcn';
19
+
20
+ export { applyShadcnAdapter, getShadcnSlotInjections, isShadcnAdapterDroppedTag } from './shadcn';
21
+
22
+ /**
23
+ * Returns true when the given JSX tagName is unconditionally dropped by
24
+ * some registered framework adapter at render time. Walks that adapters
25
+ * declare via their own `is…DroppedTag` predicate so callers don't have to
26
+ * know which adapters exist. Pre-render scans (responsive-signal
27
+ * detection, complexity heuristics, …) consult this to avoid counting
28
+ * classes on elements that never reach Figma.
29
+ */
30
+ export function isFrameworkAdapterDroppedTag(tagName: string): boolean {
31
+ return isShadcnAdapterDroppedTag(tagName);
32
+ // Future adapters add `|| isMuiAdapterDroppedTag(tagName)` etc.
33
+ }
34
+
35
+ /**
36
+ * Run every registered framework adapter against the IR in turn. Each
37
+ * adapter returns the tree by reference when it makes no changes, so the
38
+ * common case (no shadcn slots in the tree) costs only a tree-walk.
39
+ */
40
+ export function applyFrameworkAdapters(node: NodeIR): NodeIR {
41
+ let next = node;
42
+ next = applyShadcnAdapter(next);
43
+ // Add new adapters here, e.g.:
44
+ // next = applyMuiAdapter(next);
45
+ // next = applyHeadlessUiAdapter(next);
46
+ return next;
47
+ }