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
package/scanner/cli.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * that the Figma plugin uses to create components.
7
7
  *
8
8
  * Usage (consumer project, after `pnpm add -D inkbridge && pnpm exec inkbridge setup`):
9
- * pnpm figma:scan — run scanner manually
10
- * GET /api/figma/scan-components — called automatically by the Figma plugin
9
+ * pnpm inkbridge:scan — run scanner manually
10
+ * GET /api/inkbridge/scan-components — called automatically by the Figma plugin
11
11
  *
12
12
  * Usage (monorepo dev):
13
- * npx tsx tools/figma-plugin-tailwind-tokens/scanner/cli.ts
13
+ * npx tsx tools/figma-plugin/scanner/cli.ts
14
14
  */
15
15
 
16
16
  import * as fs from 'fs';
@@ -22,8 +22,9 @@ import { extractColorTokens, extractPaletteTokens, TAILWIND_SPACING, inferLayout
22
22
  import { collectAllClasses } from './class-collector';
23
23
  import { buildStyleMap } from './style-map';
24
24
  import { readTokenSourceMap } from './css-token-reader';
25
- import type { ComponentDefinitions, ScannerConfig, LayoutInfo, ResponsiveInfo, ColorSchemeInfo, IconImportSpec, IconRegistryEntry } from './types';
26
- import type { TokenSourceMode } from '../src/token-source';
25
+ import type { ComponentDefinitions, ScannerConfig, ResponsiveInfo, IconImportSpec, IconRegistryEntry, ComponentAnalysis, JsxNode, AtomicKind } from './types';
26
+ import type { TokenSourceMode } from '../src/tokens/token-source';
27
+ import { inferSafeTextOverridePropKeys } from '../src/components/symbol-instance-policy';
27
28
 
28
29
  // ============================================================================
29
30
  // CLI flags
@@ -56,6 +57,14 @@ const DEFAULT_CONFIG: ScannerConfig = {
56
57
 
57
58
  const COMPONENT_DEFS_SCHEMA_VERSION = 1;
58
59
 
60
+ type GraphMeta = {
61
+ kind: AtomicKind;
62
+ usesCount: number;
63
+ usedByCount: number;
64
+ isLeaf: boolean;
65
+ symbolCandidate: boolean;
66
+ };
67
+
59
68
  // ============================================================================
60
69
  // Main
61
70
  // ============================================================================
@@ -77,7 +86,9 @@ async function main() {
77
86
  const styleMap = await buildStyleMap(allClasses);
78
87
  const iconRegistry = await buildIconRegistry(scanner.getIconRegistry());
79
88
 
80
- // Enrich each analysis with layout, responsive, and color-scheme info
89
+ const graphMetaByName = buildGraphMetadata(analyses);
90
+
91
+ // Enrich each analysis with layout, responsive, color-scheme, and graph metadata
81
92
  const enriched = analyses.map(analysis => {
82
93
  const classes = getAllClassesFromAnalysis(analysis);
83
94
  const layout = inferLayout(classes);
@@ -88,8 +99,21 @@ async function main() {
88
99
  );
89
100
  const responsive: ResponsiveInfo = { breakpoints, hasResponsiveVisibility };
90
101
  const colorScheme = groupClassesByColorScheme(classes);
91
-
92
- return { analysis, layout, responsive, colorScheme };
102
+ const graphMeta = graphMetaByName.get(analysis.name);
103
+ const safeTextOverrideProps = inferSafeTextOverridePropKeys(analysis);
104
+
105
+ return {
106
+ analysis,
107
+ layout,
108
+ responsive,
109
+ colorScheme,
110
+ kind: graphMeta?.kind,
111
+ usesCount: graphMeta?.usesCount,
112
+ usedByCount: graphMeta?.usedByCount,
113
+ isLeaf: graphMeta?.isLeaf,
114
+ symbolCandidate: graphMeta?.symbolCandidate,
115
+ safeTextOverrideProps: safeTextOverrideProps.length > 0 ? safeTextOverrideProps : undefined,
116
+ };
93
117
  });
94
118
 
95
119
  // Build output
@@ -116,13 +140,23 @@ async function main() {
116
140
  });
117
141
  }
118
142
 
119
- // Write to JSON file
120
- const defaultOutputPath = 'tools/figma-plugin-tailwind-tokens/component-definitions.json';
143
+ // Write to JSON file. Default matches the consumer-side path used by
144
+ // the generated `scan-components` API route (see
145
+ // `templates/scan-components-route.ts`), so a manual
146
+ // `pnpm inkbridge:scan` and a plugin-triggered scan land the JSON in
147
+ // the same place. The path lives under `.inkbridge/` at the consumer
148
+ // project root; in this repo the scanner is also used internally
149
+ // (e.g. by regression fixtures) and the same path applies.
150
+ const defaultOutputPath = '.inkbridge/component-definitions.json';
121
151
  const outputPath = CLI_OUTPUT
122
152
  ? path.resolve(process.cwd(), CLI_OUTPUT)
123
153
  : path.resolve(process.cwd(), defaultOutputPath);
124
154
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
125
- fs.writeFileSync(outputPath, JSON.stringify(finalOutput, null, 2));
155
+ // Compact JSON (no pretty-print). The scan output is only read
156
+ // programmatically by the plugin runtime — the indentation cost was
157
+ // ~30% of the file size for no functional benefit. If you need to
158
+ // hand-inspect it, pipe through `jq` (`jq . component-definitions.json`).
159
+ fs.writeFileSync(outputPath, JSON.stringify(finalOutput));
126
160
 
127
161
  // Print summary
128
162
  console.log('✅ Scanned components:\n');
@@ -193,12 +227,109 @@ async function main() {
193
227
  console.log(`\n📦 Output written to: ${outputPath}`);
194
228
  console.log(` ${analyses.length} components, ${totalStories} stories, ${colorTokens.length} color tokens`);
195
229
 
230
+ if (analyses.length === 0 || totalStories === 0) {
231
+ printEmptyScanDiagnostic({
232
+ config,
233
+ componentCount: analyses.length,
234
+ storyCount: totalStories,
235
+ });
236
+ }
237
+
196
238
  } catch (err) {
197
239
  console.error('❌ Scanner failed:', err);
198
240
  process.exit(1);
199
241
  }
200
242
  }
201
243
 
244
+ /**
245
+ * Print actionable hints when the scan returns 0 components or 0 stories.
246
+ * Walks the configured component paths looking for story-like files
247
+ * (`*.stories.ts`, `*.stories.tsx`, `*.stories.jsx`, `*.stories.js`,
248
+ * `*.stories.mdx`) and names what it found vs what the scanner was
249
+ * looking for. Better than a silent "0 stories" line.
250
+ */
251
+ function printEmptyScanDiagnostic(opts: {
252
+ config: ScannerConfig;
253
+ componentCount: number;
254
+ storyCount: number;
255
+ }): void {
256
+ const { config, componentCount, storyCount } = opts;
257
+ const cwd = process.cwd();
258
+ const STORY_EXTS = ['.stories.tsx', '.stories.ts', '.stories.jsx', '.stories.js', '.stories.mdx'];
259
+ const componentExts = ['.tsx'];
260
+ const found: { stories: string[]; components: string[] } = { stories: [], components: [] };
261
+
262
+ function walk(dir: string, depth: number): void {
263
+ if (depth > 8) return;
264
+ let entries: fs.Dirent[];
265
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
266
+ catch (_e) { return; }
267
+ for (const entry of entries) {
268
+ if (entry.name === 'node_modules' || entry.name === '.next' || entry.name === '.git' || entry.name.startsWith('.')) continue;
269
+ const full = path.join(dir, entry.name);
270
+ if (entry.isDirectory()) {
271
+ walk(full, depth + 1);
272
+ } else if (entry.isFile()) {
273
+ const rel = path.relative(cwd, full);
274
+ if (STORY_EXTS.some(ext => entry.name.endsWith(ext))) {
275
+ found.stories.push(rel);
276
+ } else if (componentExts.some(ext => entry.name.endsWith(ext))) {
277
+ // Only count files that aren't tests / index barrels
278
+ if (!entry.name.endsWith('.test.tsx') && entry.name !== 'index.tsx') {
279
+ found.components.push(rel);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ for (const cp of config.componentPaths) {
287
+ const abs = path.resolve(cwd, cp);
288
+ if (fs.existsSync(abs)) walk(abs, 0);
289
+ }
290
+
291
+ console.log('');
292
+ if (componentCount === 0) {
293
+ console.log('⚠️ No components matched. Scanner looked under:');
294
+ for (const cp of config.componentPaths) {
295
+ const abs = path.resolve(cwd, cp);
296
+ const exists = fs.existsSync(abs);
297
+ console.log(` - ${cp} ${exists ? '' : '(missing)'}`);
298
+ }
299
+ if (found.components.length > 0) {
300
+ console.log('');
301
+ console.log(` Found ${found.components.length} .tsx file(s) but none qualified as components. Check`);
302
+ console.log(' `inkbridge.config.json` — `componentPaths`, `exclude`, and `onlyWithStories`.');
303
+ }
304
+ } else if (storyCount === 0) {
305
+ console.log(`⚠️ Found ${componentCount} component(s) but 0 stories.`);
306
+ console.log(' Scanner pairs `<Component>.tsx` with `<Component>.stories.tsx`');
307
+ console.log(' or `<Component>.stories.ts` in the same folder.');
308
+ if (found.stories.length > 0) {
309
+ const wrongExt = found.stories.filter(f => f.endsWith('.stories.jsx') || f.endsWith('.stories.js') || f.endsWith('.stories.mdx'));
310
+ if (wrongExt.length > 0) {
311
+ console.log('');
312
+ console.log(' Found story files with unsupported extensions:');
313
+ for (const f of wrongExt.slice(0, 10)) console.log(` - ${f}`);
314
+ if (wrongExt.length > 10) console.log(` … and ${wrongExt.length - 10} more`);
315
+ console.log(' Rename to `.stories.tsx` (preferred) or `.stories.ts`.');
316
+ } else {
317
+ console.log('');
318
+ console.log(' Story files exist but none are co-located with a component:');
319
+ for (const f of found.stories.slice(0, 10)) console.log(` - ${f}`);
320
+ if (found.stories.length > 10) console.log(` … and ${found.stories.length - 10} more`);
321
+ console.log(' Move each story next to its component, or update');
322
+ console.log(' `inkbridge.config.json:componentPaths` to include the story folder.');
323
+ }
324
+ } else {
325
+ console.log('');
326
+ console.log(' No story files found anywhere under the component paths.');
327
+ console.log(' Add one (e.g. `src/components/Button/Button.stories.tsx`) and re-run.');
328
+ }
329
+ }
330
+ console.log('');
331
+ }
332
+
202
333
  /**
203
334
  * Extract all classes from any component analysis type
204
335
  */
@@ -226,6 +357,204 @@ function getAllClassesFromAnalysis(analysis: import('./types').ComponentAnalysis
226
357
  return classes;
227
358
  }
228
359
 
360
+ type StructureMetrics = {
361
+ elementCount: number;
362
+ componentNodeCount: number;
363
+ maxDepth: number;
364
+ };
365
+
366
+ function normalizeComponentName(name: string): string {
367
+ return String(name || '')
368
+ .replace(/[^a-zA-Z0-9]/g, '')
369
+ .toLowerCase();
370
+ }
371
+
372
+ function collectStoryRefsFromJsxTree(node: JsxNode | undefined, out: Set<string>): void {
373
+ if (!node || node.type !== 'element') return;
374
+ const tagName = String(node.tagName || '');
375
+ const isComponent = !!node.isComponent || /^[A-Z]/.test(tagName);
376
+ if (isComponent && tagName) {
377
+ out.add(tagName);
378
+ }
379
+ for (let i = 0; i < node.children.length; i++) {
380
+ collectStoryRefsFromJsxTree(node.children[i], out);
381
+ }
382
+ }
383
+
384
+ function collectStructureMetricsFromJsxTree(node: JsxNode | undefined, depth: number, metrics: StructureMetrics): void {
385
+ if (!node || node.type !== 'element') return;
386
+ metrics.elementCount++;
387
+ metrics.maxDepth = Math.max(metrics.maxDepth, depth);
388
+ if (node.isComponent || /^[A-Z]/.test(String(node.tagName || ''))) {
389
+ metrics.componentNodeCount++;
390
+ }
391
+ for (let i = 0; i < node.children.length; i++) {
392
+ collectStructureMetricsFromJsxTree(node.children[i], depth + 1, metrics);
393
+ }
394
+ }
395
+
396
+ function collectStoryComponentRefs(analysis: ComponentAnalysis): Set<string> {
397
+ const refs = new Set<string>();
398
+ const stories = analysis.stories || [];
399
+ for (let i = 0; i < stories.length; i++) {
400
+ const story = stories[i];
401
+ const instances = story.instances || [];
402
+ for (let j = 0; j < instances.length; j++) {
403
+ const ref = String(instances[j]?.componentName || '').trim();
404
+ if (ref) refs.add(ref);
405
+ }
406
+ collectStoryRefsFromJsxTree(story.jsxTree, refs);
407
+ }
408
+ return refs;
409
+ }
410
+
411
+ function collectStoryStructureMetrics(analysis: ComponentAnalysis): StructureMetrics {
412
+ const metrics: StructureMetrics = { elementCount: 0, componentNodeCount: 0, maxDepth: 0 };
413
+ const stories = analysis.stories || [];
414
+ for (let i = 0; i < stories.length; i++) {
415
+ const perStory: StructureMetrics = { elementCount: 0, componentNodeCount: 0, maxDepth: 0 };
416
+ collectStructureMetricsFromJsxTree(stories[i].jsxTree, 1, perStory);
417
+ metrics.elementCount = Math.max(metrics.elementCount, perStory.elementCount);
418
+ metrics.componentNodeCount = Math.max(metrics.componentNodeCount, perStory.componentNodeCount);
419
+ metrics.maxDepth = Math.max(metrics.maxDepth, perStory.maxDepth);
420
+ }
421
+ return metrics;
422
+ }
423
+
424
+ function createAnalysisResolver(analyses: ComponentAnalysis[]): (refName: string) => string | null {
425
+ const byName = new Map<string, string>();
426
+ const byNormalized = new Map<string, string[]>();
427
+
428
+ for (let i = 0; i < analyses.length; i++) {
429
+ const name = analyses[i].name;
430
+ if (!name) continue;
431
+ byName.set(name, name);
432
+ const normalized = normalizeComponentName(name);
433
+ const list = byNormalized.get(normalized) || [];
434
+ list.push(name);
435
+ byNormalized.set(normalized, list);
436
+ }
437
+
438
+ return function resolve(refName: string): string | null {
439
+ const ref = String(refName || '').trim();
440
+ if (!ref) return null;
441
+ if (byName.has(ref)) return byName.get(ref) || null;
442
+
443
+ const normalized = normalizeComponentName(ref);
444
+ const direct = byNormalized.get(normalized);
445
+ if (direct && direct.length === 1) return direct[0];
446
+ return null;
447
+ };
448
+ }
449
+
450
+ function inferAtomicKind(
451
+ analysis: ComponentAnalysis,
452
+ metrics: StructureMetrics,
453
+ usesCount: number,
454
+ usedByCount: number
455
+ ): AtomicKind {
456
+ const hasStory = !!analysis.hasStory;
457
+ const isLeaf = usesCount === 0;
458
+ const classCount = getAllClassesFromAnalysis(analysis).length;
459
+ const isPrimitiveType = analysis.type === 'cva' || analysis.type === 'state';
460
+ const hasComposition = usesCount > 0 || metrics.componentNodeCount > 0;
461
+ const isHighComplexity =
462
+ metrics.maxDepth >= 5
463
+ || metrics.elementCount >= 24
464
+ || (hasComposition && metrics.elementCount >= 16)
465
+ || usesCount >= 2;
466
+
467
+ if (!hasStory && usedByCount === 0 && isLeaf) {
468
+ return 'utility';
469
+ }
470
+
471
+ if (isPrimitiveType) {
472
+ return 'atom';
473
+ }
474
+
475
+ if (isLeaf && usedByCount > 0 && metrics.maxDepth <= 3 && metrics.elementCount <= 10) {
476
+ return 'atom';
477
+ }
478
+
479
+ if (isHighComplexity) {
480
+ return 'organism';
481
+ }
482
+
483
+ if (analysis.type === 'compound' || hasComposition || usedByCount > 0) {
484
+ return 'molecule';
485
+ }
486
+
487
+ if (isLeaf && classCount > 0 && metrics.maxDepth <= 2) {
488
+ return 'atom';
489
+ }
490
+
491
+ return 'other';
492
+ }
493
+
494
+ function isSymbolCandidate(kind: AtomicKind, analysis: ComponentAnalysis, usedByCount: number): boolean {
495
+ if (analysis.type === 'cva' || analysis.type === 'state') {
496
+ return kind === 'atom' || kind === 'molecule';
497
+ }
498
+ if (analysis.type === 'compound') {
499
+ return kind === 'molecule' || kind === 'atom';
500
+ }
501
+ if (analysis.type === 'simple') {
502
+ return (kind === 'atom' || kind === 'molecule') && usedByCount > 0;
503
+ }
504
+ return false;
505
+ }
506
+
507
+ function buildGraphMetadata(analyses: ComponentAnalysis[]): Map<string, GraphMeta> {
508
+ const outgoing = new Map<string, Set<string>>();
509
+ const incoming = new Map<string, Set<string>>();
510
+ const metricsByName = new Map<string, StructureMetrics>();
511
+ const resolveAnalysisName = createAnalysisResolver(analyses);
512
+
513
+ for (let i = 0; i < analyses.length; i++) {
514
+ const analysis = analyses[i];
515
+ outgoing.set(analysis.name, new Set<string>());
516
+ incoming.set(analysis.name, new Set<string>());
517
+ metricsByName.set(analysis.name, collectStoryStructureMetrics(analysis));
518
+ }
519
+
520
+ for (let i = 0; i < analyses.length; i++) {
521
+ const analysis = analyses[i];
522
+ const refs = collectStoryComponentRefs(analysis);
523
+ const from = outgoing.get(analysis.name);
524
+ if (!from) continue;
525
+ refs.forEach((refName) => {
526
+ const target = resolveAnalysisName(refName);
527
+ if (!target || target === analysis.name) return;
528
+ from.add(target);
529
+ const targetIncoming = incoming.get(target);
530
+ if (targetIncoming) targetIncoming.add(analysis.name);
531
+ });
532
+ }
533
+
534
+ const out = new Map<string, GraphMeta>();
535
+ for (let i = 0; i < analyses.length; i++) {
536
+ const analysis = analyses[i];
537
+ const usesCount = outgoing.get(analysis.name)?.size || 0;
538
+ const usedByCount = incoming.get(analysis.name)?.size || 0;
539
+ const isLeaf = usesCount === 0;
540
+ const metrics = metricsByName.get(analysis.name) || {
541
+ elementCount: 0,
542
+ componentNodeCount: 0,
543
+ maxDepth: 0,
544
+ };
545
+ const kind = inferAtomicKind(analysis, metrics, usesCount, usedByCount);
546
+ out.set(analysis.name, {
547
+ kind,
548
+ usesCount,
549
+ usedByCount,
550
+ isLeaf,
551
+ symbolCandidate: isSymbolCandidate(kind, analysis, usedByCount),
552
+ });
553
+ }
554
+
555
+ return out;
556
+ }
557
+
229
558
  /**
230
559
  * Extract component paths from Storybook's stories globs.
231
560
  * e.g. "../src/**\/\*.stories.@(ts|tsx)" → ["src"]
@@ -304,8 +633,8 @@ async function buildIconRegistry(imports: Record<string, IconImportSpec>): Promi
304
633
  const spec = imports[localName];
305
634
  if (!spec) continue;
306
635
  try {
307
- const mod = await import(spec.module);
308
- const IconComponent = (mod as any)[spec.exportName];
636
+ const mod = (await import(spec.module)) as Record<string, React.ComponentType<{ size?: number; color?: string }>>;
637
+ const IconComponent = mod[spec.exportName];
309
638
  if (!IconComponent) continue;
310
639
  const element = React.createElement(IconComponent, { size: 24, color: '#000' });
311
640
  let svg = renderToStaticMarkup(element);