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,172 @@
1
+ import { splitClassName } from '../tailwind';
2
+ import { MASTER_ICON_NAME_KEY } from '../components/component-instance';
3
+ import { getThemeContext } from './theme-context';
4
+ import { buildCvaClassesWithSelection } from './cva-utils';
5
+ import {
6
+ normalizeComponentName,
7
+ findBestStoryElementForClasses,
8
+ getStoriesWithJsxTree,
9
+ countClassOverlap,
10
+ findMatchingInstance,
11
+ } from './story-tree-search';
12
+ import type { StoryBuilderContext } from './story-builder-context';
13
+
14
+ /**
15
+ * Builders for the source nodes that wrap each variant of a generated CVA
16
+ * component set. Given a story's JSX tree and a target variant selection,
17
+ * `buildCvaSymbolSourceNode` finds the best-matching element, overrides
18
+ * its className with the resolved variant classes (so the master renders
19
+ * the exact variant), and renders it through `ctx.renderJsxTree`.
20
+ *
21
+ * `buildNonCvaSymbolSourceNode` (state / simple / compound masters) still
22
+ * lives in story-builder.ts because it depends on createStateStoryFrame /
23
+ * createSimpleStoryFrame — those move out together in phase 6b.
24
+ */
25
+
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ export function renderStoryElementAsSymbolSource(node: any, theme: string, ctx: StoryBuilderContext): any | null {
28
+ if (!node || node.type !== 'element') return null;
29
+ const themeContext = getThemeContext(theme);
30
+ const colorGroup = themeContext.colorGroup;
31
+ const radiusGroup = themeContext.radiusGroup;
32
+ return ctx.renderJsxTree(node, colorGroup, radiusGroup, theme, 0, {
33
+ parentLayout: 'VERTICAL',
34
+ });
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ function getCvaVariantValueForStory(def: any, story: any, variantKey: string): string {
39
+ const instance = findMatchingInstance(def, story);
40
+ const props = instance && instance.props ? instance.props : {};
41
+ if (props[variantKey] != null && String(props[variantKey]) !== '') {
42
+ return String(props[variantKey]);
43
+ }
44
+ if (def && def.defaultVariants && def.defaultVariants[variantKey] != null) {
45
+ return String(def.defaultVariants[variantKey]);
46
+ }
47
+ const variants = def && def.variants ? def.variants : {};
48
+ if (variants[variantKey] && variants[variantKey][0] != null) {
49
+ return String(variants[variantKey][0]);
50
+ }
51
+ return '';
52
+ }
53
+
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ function storyMatchesCvaSelection(def: any, story: any, selection: Record<string, string>): boolean {
56
+ const variantKeys = Object.keys((def && def.variants) || {});
57
+ if (variantKeys.length === 0) return false;
58
+ for (let i = 0; i < variantKeys.length; i++) {
59
+ const key = variantKeys[i];
60
+ const storyValue = getCvaVariantValueForStory(def, story, key);
61
+ const selectionValue = selection[key] != null ? String(selection[key]) : '';
62
+ if (selectionValue && storyValue !== selectionValue) return false;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ function overrideClassNameForCvaTarget(node: any, def: any, targetClasses: string[]): any {
69
+ if (!node || node.type !== 'element') return node;
70
+ const existing = splitClassName(node.props && node.props.className);
71
+ const existingSet = new Set(existing);
72
+ let identical = existing.length === targetClasses.length;
73
+ if (identical) {
74
+ for (let i = 0; i < targetClasses.length; i++) {
75
+ if (!existingSet.has(targetClasses[i])) { identical = false; break; }
76
+ }
77
+ }
78
+ if (identical) return node;
79
+ const nextProps = Object.assign({}, node.props || {});
80
+ nextProps.className = targetClasses.join(' ');
81
+ // Also strip CVA variant prop values from the scanned node — the target
82
+ // classes already encode the variant, so leaving stale variant props can
83
+ // confuse downstream class inference.
84
+ const variantKeys = Object.keys((def && def.variants) || {});
85
+ for (let i = 0; i < variantKeys.length; i++) delete nextProps[variantKeys[i]];
86
+ return Object.assign({}, node, { props: nextProps });
87
+ }
88
+
89
+ export function buildCvaSymbolSourceNode(
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ def: any,
92
+ selection: Record<string, string>,
93
+ theme: string,
94
+ ctx: StoryBuilderContext
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ ): any | null {
97
+ const stories = getStoriesWithJsxTree(def);
98
+ if (stories.length === 0) return null;
99
+
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ const orderedStories = stories.slice().sort(function(a: any, b: any): number {
102
+ const aScore = storyMatchesCvaSelection(def, a, selection) ? 0 : 1;
103
+ const bScore = storyMatchesCvaSelection(def, b, selection) ? 0 : 1;
104
+ return aScore - bScore;
105
+ });
106
+ const targetClasses = buildCvaClassesWithSelection(def, selection, '');
107
+ const targetSet = new Set(targetClasses);
108
+ const selfName = normalizeComponentName(def && def.name ? def.name : '');
109
+
110
+ for (let i = 0; i < orderedStories.length; i++) {
111
+ const story = orderedStories[i];
112
+ const tree = story && story.jsxTree;
113
+ if (!tree || tree.type !== 'element') continue;
114
+
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ let nodeToRender: any | null = null;
117
+ const candidate = findBestStoryElementForClasses(tree, targetClasses);
118
+ const candidateIsSelfComponent = !!(
119
+ candidate
120
+ && candidate.type === 'element'
121
+ && candidate.isComponent
122
+ && normalizeComponentName(candidate.tagName) === selfName
123
+ );
124
+ if (candidate && candidate.type === 'element' && !candidateIsSelfComponent) {
125
+ nodeToRender = candidate;
126
+ } else {
127
+ const rootClasses = splitClassName(tree.props && tree.props.className);
128
+ const overlap = countClassOverlap(rootClasses, targetSet);
129
+ const minOverlap = Math.max(1, Math.min(4, Math.floor(targetClasses.length * 0.35)));
130
+ if (overlap >= minOverlap) {
131
+ nodeToRender = tree;
132
+ }
133
+ }
134
+ if (!nodeToRender) continue;
135
+
136
+ // If the picked element isn't an exact match for this variant+size
137
+ // selection (e.g. Outline button from the Variants story, which is at
138
+ // default size), override its className with the target variant's
139
+ // resolved classes so the master's component for this variant renders
140
+ // at the right size/style. Otherwise the structure comes from a
141
+ // mismatched source and the variant says "Sm" but renders at h-9.
142
+ const nodeForRender = overrideClassNameForCvaTarget(nodeToRender, def, targetClasses);
143
+ const sourceNode = renderStoryElementAsSymbolSource(nodeForRender, theme, ctx);
144
+ if (sourceNode) {
145
+ // Stamp the icon name on the rendered source node so the master
146
+ // variant Component (built in cva-master.ts) can propagate it. The
147
+ // CVA-instance creator reads this to decide whether per-instance
148
+ // element children (e.g. icons) match the master and can reuse it,
149
+ // or differ and need frame-rendering fall-back.
150
+ const iconName = findFirstElementChildTagName(nodeForRender);
151
+ if (iconName) {
152
+ try { sourceNode.setPluginData(MASTER_ICON_NAME_KEY, iconName); } catch (_e) { /* ignore */ }
153
+ }
154
+ return sourceNode;
155
+ }
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ function findFirstElementChildTagName(node: any): string | null {
163
+ if (!node || typeof node !== 'object') return null;
164
+ const children = Array.isArray(node.children) ? node.children : [];
165
+ for (let i = 0; i < children.length; i++) {
166
+ const c = children[i];
167
+ if (c && c.type === 'element' && typeof c.tagName === 'string' && c.tagName.length > 0) {
168
+ return c.tagName;
169
+ }
170
+ }
171
+ return null;
172
+ }
@@ -0,0 +1,56 @@
1
+ import { getBaseClass } from '../tailwind';
2
+ import type { NodeIR } from '../tailwind';
3
+
4
+ /**
5
+ * Helpers for rendering semantic HTML table primitives (table / thead /
6
+ * tbody / tfoot / tr / td / th) into Figma's auto-layout model. Borders
7
+ * are cleared selectively to avoid the double-border seam that two
8
+ * adjacent cells would otherwise produce.
9
+ */
10
+
11
+ /**
12
+ * True when `classes` contains a width override that should bypass the
13
+ * default "fill the row" behaviour of a table cell — e.g. an explicit
14
+ * `w-32`, `flex-1`, or `min-w-*`.
15
+ */
16
+ export function hasTableCellSizeOverride(classes: string[]): boolean {
17
+ for (const cls of classes) {
18
+ const base = getBaseClass(cls);
19
+ if (!base) continue;
20
+ if (base === 'flex-1' || base === 'grow' || base === 'grow-0') return true;
21
+ if (base.startsWith('w-') || base.startsWith('min-w-') || base.startsWith('max-w-')) return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ export function getNumericColSpan(value: unknown): number {
27
+ if (typeof value === 'number' && Number.isFinite(value)) return Math.max(1, Math.floor(value));
28
+ if (typeof value === 'string') {
29
+ const parsed = parseInt(value, 10);
30
+ if (Number.isFinite(parsed)) return Math.max(1, parsed);
31
+ }
32
+ return 1;
33
+ }
34
+
35
+ export function isSemanticTableContainer(node: NodeIR): boolean {
36
+ if (node.kind !== 'element' && node.kind !== 'component') return false;
37
+ return node.tagLower === 'table' || node.tagLower === 'thead' || node.tagLower === 'tbody' || node.tagLower === 'tfoot';
38
+ }
39
+
40
+ export function clearTopBorder(node: SceneNode): void {
41
+ if (!('strokeTopWeight' in node)) return;
42
+ try {
43
+ node.strokeTopWeight = 0;
44
+ } catch (_err) {
45
+ // ignore if unsupported on this specific subtype
46
+ }
47
+ }
48
+
49
+ export function clearBottomBorder(node: SceneNode): void {
50
+ if (!('strokeBottomWeight' in node)) return;
51
+ try {
52
+ node.strokeBottomWeight = 0;
53
+ } catch (_err) {
54
+ // ignore if unsupported on this specific subtype
55
+ }
56
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Pure tag-name predicates for the Radix / Base UI primitive components
3
+ * the plugin recognises (Accordion, Tabs, Select, RadioGroup, Portal-rooted
4
+ * popover panels). Used by ui-builder and any future renderer that needs
5
+ * to branch on component family without inlining the string lists.
6
+ *
7
+ * No Figma dependencies, no shared state — pure string matching.
8
+ */
9
+
10
+ export function isAccordionRootTag(tagName: string): boolean {
11
+ return tagName === 'Accordion' || tagName === 'AccordionPrimitive.Root';
12
+ }
13
+
14
+ export function isAccordionItemTag(tagName: string): boolean {
15
+ return tagName === 'AccordionItem' || tagName === 'AccordionPrimitive.Item';
16
+ }
17
+
18
+ export function isAccordionContentTag(tagName: string): boolean {
19
+ return tagName === 'AccordionContent' || tagName === 'AccordionPrimitive.Content';
20
+ }
21
+
22
+ export function isAccordionHeaderTag(tagName: string): boolean {
23
+ return tagName === 'AccordionHeader' || tagName === 'AccordionPrimitive.Header';
24
+ }
25
+
26
+ export function isRadioGroupRootTag(tagName: string): boolean {
27
+ return tagName === 'RadioGroup' || tagName === 'RadioGroupPrimitive' || tagName === 'RadioGroupPrimitive.Root';
28
+ }
29
+
30
+ export function isRadioGroupItemTag(tagName: string): boolean {
31
+ return tagName === 'RadioGroupItem' || tagName === 'RadioPrimitive.Root' || tagName === 'RadioGroupPrimitive.Item';
32
+ }
33
+
34
+ export function isRadioGroupIndicatorTag(tagName: string): boolean {
35
+ return tagName === 'RadioPrimitive.Indicator' || tagName === 'RadioGroupPrimitive.Indicator';
36
+ }
37
+
38
+ export function isTabsRootTag(tagName: string): boolean {
39
+ return tagName === 'Tabs' || tagName === 'TabsPrimitive.Root';
40
+ }
41
+
42
+ export function isTabsTriggerTag(tagName: string): boolean {
43
+ return tagName === 'TabsTrigger' || tagName === 'TabsPrimitive.Trigger' || tagName === 'TabsPrimitive.Tab';
44
+ }
45
+
46
+ export function isTabsListTag(tagName: string): boolean {
47
+ return tagName === 'TabsList' || tagName === 'TabsPrimitive.List';
48
+ }
49
+
50
+ export function isTabsContentTag(tagName: string): boolean {
51
+ return tagName === 'TabsContent' || tagName === 'TabsPrimitive.Content' || tagName === 'TabsPrimitive.Panel';
52
+ }
53
+
54
+ export function isSelectRootTag(tagName: string): boolean {
55
+ return tagName === 'Select' || tagName === 'SelectPrimitive.Root';
56
+ }
57
+
58
+ export function isSelectContentTag(tagName: string): boolean {
59
+ return tagName === 'SelectContent' || tagName === 'SelectPrimitive.Content';
60
+ }
61
+
62
+ export function isSelectTriggerTag(tagName: string): boolean {
63
+ return tagName === 'SelectTrigger' || tagName === 'SelectPrimitive.Trigger';
64
+ }
65
+
66
+ export function isSelectValueTag(tagName: string): boolean {
67
+ return tagName === 'SelectValue' || tagName === 'SelectPrimitive.Value';
68
+ }
69
+
70
+ export function isSelectItemTag(tagName: string): boolean {
71
+ return tagName === 'SelectItem' || tagName === 'SelectPrimitive.Item';
72
+ }
73
+
74
+ export function isSelectItemIndicatorTag(tagName: string): boolean {
75
+ return tagName === 'SelectPrimitive.ItemIndicator' || tagName === 'SelectItemIndicator';
76
+ }
77
+
78
+ /**
79
+ * Radix & Base UI portal components (Popover/Tooltip/DropdownMenu/HoverCard/Menu)
80
+ * expose an `Arrow` subcomponent that visually points at the trigger. In
81
+ * Figma the panel isn't anchored to the trigger, so the arrow is noise.
82
+ */
83
+ export function isPortalArrowTag(tagName: string): boolean {
84
+ if (!tagName) return false;
85
+ const lower = tagName.toLowerCase();
86
+ return lower.endsWith('.arrow')
87
+ || lower === 'popoverarrow'
88
+ || lower === 'tooltiparrow'
89
+ || lower === 'dropdownmenuarrow'
90
+ || lower === 'hovercardarrow'
91
+ || lower === 'menuarrow';
92
+ }
93
+
94
+ export function isSelectScrollButtonTag(tagName: string): boolean {
95
+ return tagName === 'SelectScrollUpButton'
96
+ || tagName === 'SelectPrimitive.ScrollUpButton'
97
+ || tagName === 'SelectScrollDownButton'
98
+ || tagName === 'SelectPrimitive.ScrollDownButton';
99
+ }
@@ -0,0 +1,52 @@
1
+ import { TOKENS, getThemeColors, getThemeRadius } from '../tokens';
2
+
3
+ /**
4
+ * Per-theme rendering context, cached so the design-system page builder
5
+ * doesn't re-resolve TOKENS on every story it renders.
6
+ */
7
+ export type ThemeContext = {
8
+ theme: string;
9
+ colorGroup: Record<string, string>;
10
+ radiusGroup: Record<string, string> | null;
11
+ };
12
+
13
+ const THEME_CONTEXT_CACHE: Record<string, ThemeContext> = {};
14
+
15
+ export function getThemeContext(theme: string): ThemeContext {
16
+ if (THEME_CONTEXT_CACHE[theme]) return THEME_CONTEXT_CACHE[theme];
17
+ const colorGroup = getThemeColors(TOKENS, theme);
18
+ const radiusGroup = getThemeRadius(TOKENS, theme);
19
+ const ctx: ThemeContext = {
20
+ theme: theme,
21
+ colorGroup: colorGroup,
22
+ radiusGroup: radiusGroup,
23
+ };
24
+ THEME_CONTEXT_CACHE[theme] = ctx;
25
+ return ctx;
26
+ }
27
+
28
+ /**
29
+ * Spacing constants for the Design System page layout. Used by both
30
+ * story-builder and preview-builder so column widths, axis gaps, and
31
+ * matrix paddings stay consistent across boards.
32
+ */
33
+ export const BOARD_LAYOUT = {
34
+ boardGap: 96,
35
+ sectionPaddingX: 64,
36
+ sectionPaddingY: 64,
37
+ columnsGap: 320,
38
+ columnGap: 144,
39
+ columnPaddingX: 0,
40
+ columnPaddingY: 160,
41
+ sectionGroupGap: 176,
42
+ sectionTitleGap: 48,
43
+ componentBlockGap: 128,
44
+ componentTitleGap: 48,
45
+ storyGap: 32,
46
+ showcaseGap: 128,
47
+ stateMatrixGap: 40,
48
+ stateMatrixAxisGap: 64,
49
+ responsiveBlockGap: 32,
50
+ responsiveColumnGap: 64,
51
+ responsiveLabelGap: 20,
52
+ };
@@ -0,0 +1,100 @@
1
+ import { parseUtilityClass } from '../tailwind';
2
+
3
+ /**
4
+ * Tailwind text-* utility → Figma font-size (px). Shared lookup; importers
5
+ * read it directly when matching raw classes (the parsed-utility variants
6
+ * `getPreviewTypography` handles via `parseUtilityClass` first).
7
+ */
8
+ export const TEXT_SIZE_PX: Record<string, number> = {
9
+ 'text-xs': 12,
10
+ 'text-sm': 14,
11
+ 'text-base': 16,
12
+ 'text-lg': 18,
13
+ 'text-xl': 20,
14
+ 'text-2xl': 24,
15
+ 'text-3xl': 30,
16
+ 'text-4xl': 36,
17
+ 'text-5xl': 48,
18
+ };
19
+
20
+ /**
21
+ * Numeric ordering of `font-*` weight utilities. Used to decide
22
+ * "≥ semibold = bold" without enumerating the seven matching utilities.
23
+ */
24
+ export const FONT_WEIGHT_RANK: Record<string, number> = {
25
+ 'font-thin': 1,
26
+ 'font-extralight': 2,
27
+ 'font-light': 3,
28
+ 'font-normal': 4,
29
+ 'font-medium': 5,
30
+ 'font-semibold': 6,
31
+ 'font-bold': 7,
32
+ 'font-extrabold': 8,
33
+ 'font-black': 9,
34
+ };
35
+
36
+ /**
37
+ * Tailwind weight class → Figma font-style string. Used by the renderer
38
+ * when assigning a font's style face (`Inter:Medium`, `Open Sans:Bold`, …).
39
+ */
40
+ export const FONT_WEIGHT_TO_STYLE: Record<string, string> = {
41
+ 'font-thin': 'Thin',
42
+ 'font-extralight': 'ExtraLight',
43
+ 'font-light': 'Light',
44
+ 'font-normal': 'Regular',
45
+ 'font-medium': 'Medium',
46
+ 'font-semibold': 'SemiBold',
47
+ 'font-bold': 'Bold',
48
+ 'font-extrabold': 'ExtraBold',
49
+ 'font-black': 'Black',
50
+ };
51
+
52
+ /**
53
+ * Resolves Tailwind text-* and font-* classes to Figma typography defaults
54
+ * (font size in px, bold flag). Shared by preview-builder, cva-master and
55
+ * the story-instance render paths so they all surface identical typography
56
+ * for the same class set.
57
+ */
58
+ export function getPreviewTypography(classes: string[]): { fontSize: number; bold: boolean } {
59
+ let fontSize = 14;
60
+ let bold = false;
61
+ for (let i = 0; i < classes.length; i++) {
62
+ const atom = parseUtilityClass(classes[i]);
63
+ const utility = atom.utility || '';
64
+ if (TEXT_SIZE_PX[utility] != null) fontSize = TEXT_SIZE_PX[utility];
65
+ if (utility === 'font-bold' || utility === 'font-semibold') bold = true;
66
+ if (utility === 'font-normal' || utility === 'font-light' || utility === 'font-extralight') bold = false;
67
+ }
68
+ return { fontSize: fontSize, bold: bold };
69
+ }
70
+
71
+ export function getFontStyleFromClasses(classes: string[], fallback?: string): string | undefined {
72
+ let found: string | undefined;
73
+ for (const cls of classes) {
74
+ const style = FONT_WEIGHT_TO_STYLE[cls];
75
+ if (style) { found = style; }
76
+ }
77
+ if (found == null && fallback == null) return undefined;
78
+ return found != null ? found : fallback;
79
+ }
80
+
81
+ export function getResolvedTextSizeFromClasses(classes: string[], fallback?: number): number | undefined {
82
+ let resolved = fallback;
83
+ for (const cls of classes) {
84
+ if (TEXT_SIZE_PX[cls] != null) resolved = TEXT_SIZE_PX[cls];
85
+ }
86
+ return resolved;
87
+ }
88
+
89
+ export function getResolvedBoldFromClasses(classes: string[], fallback?: boolean): boolean | undefined {
90
+ let rank = fallback ? 7 : 4;
91
+ let seenWeight = false;
92
+ for (const cls of classes) {
93
+ if (FONT_WEIGHT_RANK[cls] != null) {
94
+ rank = FONT_WEIGHT_RANK[cls];
95
+ seenWeight = true;
96
+ }
97
+ }
98
+ if (!seenWeight && fallback == null) return undefined;
99
+ return rank >= FONT_WEIGHT_RANK['font-semibold'];
100
+ }