@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * ContractPanel component - displays SegmentContract metadata for AI agents.
3
+ * Shows propsSummary, scenarioTags, bans, and a11yRules.
4
+ */
5
+
6
+ import { memo } from 'react';
7
+ import type { SegmentContract } from '../../core/index.js';
8
+
9
+ interface ContractPanelProps {
10
+ contract?: SegmentContract;
11
+ componentName: string;
12
+ }
13
+
14
+ export const ContractPanel = memo(function ContractPanel({
15
+ contract,
16
+ componentName,
17
+ }: ContractPanelProps) {
18
+ // Empty state when no contract metadata
19
+ if (!contract || isContractEmpty(contract)) {
20
+ return (
21
+ <div className="p-6 text-center">
22
+ <div className="text-tertiary mb-4">
23
+ <svg
24
+ className="w-12 h-12 mx-auto mb-4 opacity-50"
25
+ fill="none"
26
+ viewBox="0 0 24 24"
27
+ stroke="currentColor"
28
+ >
29
+ <path
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ strokeWidth={1.5}
33
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
34
+ />
35
+ </svg>
36
+ <p className="text-sm font-medium">No contract metadata</p>
37
+ </div>
38
+ <p className="text-xs text-quaternary mb-4">
39
+ Add a <code className="bg-[--bg-hover] px-1 py-0.5 rounded">contract</code> field to the segment definition to enable AI agent features.
40
+ </p>
41
+ <div className="text-left bg-[--bg-hover] rounded-lg p-4 text-xs font-mono">
42
+ <pre className="text-quaternary">{`contract: {
43
+ propsSummary: ['variant: primary|secondary'],
44
+ scenarioTags: ['form.submit', 'action.primary'],
45
+ a11yRules: ['A11Y_BTN_LABEL'],
46
+ }`}</pre>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <div className="p-4 space-y-6 text-sm">
54
+ {/* Props Summary */}
55
+ {contract.propsSummary && contract.propsSummary.length > 0 && (
56
+ <Section title="Props Summary">
57
+ <div className="overflow-x-auto">
58
+ <table className="w-full text-xs border-collapse">
59
+ <thead>
60
+ <tr className="border-b border-[--border]">
61
+ <th className="text-left py-2 pr-4 font-medium text-tertiary">Name</th>
62
+ <th className="text-left py-2 pr-4 font-medium text-tertiary">Type</th>
63
+ <th className="text-left py-2 font-medium text-tertiary">Description</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody>
67
+ {contract.propsSummary.map((prop, i) => {
68
+ const parsed = parsePropSummary(prop);
69
+ return (
70
+ <tr key={i} className="border-b border-[--border-subtle] last:border-b-0">
71
+ <td className="py-2 pr-4 align-top">
72
+ <code className="text-xs font-medium text-primary">{parsed.name}</code>
73
+ </td>
74
+ <td className="py-2 pr-4 align-top">
75
+ <code className="text-xs text-purple-600 dark:text-purple-400">{parsed.type}</code>
76
+ </td>
77
+ <td className="py-2 align-top text-secondary">{parsed.description}</td>
78
+ </tr>
79
+ );
80
+ })}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ </Section>
85
+ )}
86
+
87
+ {/* Scenario Tags */}
88
+ {contract.scenarioTags && contract.scenarioTags.length > 0 && (
89
+ <Section title="Scenario Tags" subtitle="Used by fragments_suggest for AI queries">
90
+ <div className="flex flex-wrap gap-2">
91
+ {contract.scenarioTags.map((tag, i) => (
92
+ <span
93
+ key={i}
94
+ className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-blue-500/10 text-blue-500 border border-blue-500/20"
95
+ >
96
+ {tag}
97
+ </span>
98
+ ))}
99
+ </div>
100
+ </Section>
101
+ )}
102
+
103
+ {/* Accessibility Rules */}
104
+ {contract.a11yRules && contract.a11yRules.length > 0 && (
105
+ <Section title="Accessibility Rules">
106
+ <div className="space-y-2">
107
+ {contract.a11yRules.map((rule, i) => (
108
+ <div
109
+ key={i}
110
+ className="flex items-center gap-2 p-2 bg-[--bg-hover] rounded"
111
+ >
112
+ <span className="text-green-500">
113
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
114
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
115
+ </svg>
116
+ </span>
117
+ <code className="text-xs text-secondary">{rule}</code>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </Section>
122
+ )}
123
+
124
+ {/* Bans */}
125
+ {contract.bans && contract.bans.length > 0 && (
126
+ <Section title="Usage Bans" subtitle="Patterns to avoid">
127
+ <div className="space-y-2">
128
+ {contract.bans.map((ban, i) => (
129
+ <div
130
+ key={i}
131
+ className="p-3 bg-amber-500/10 border border-amber-500/20 rounded"
132
+ >
133
+ <div className="flex items-center gap-2 mb-1">
134
+ <span className="text-amber-500">
135
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
136
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
137
+ </svg>
138
+ </span>
139
+ <code className="text-xs text-amber-600 dark:text-amber-400">
140
+ {ban.pattern}
141
+ </code>
142
+ </div>
143
+ <p className="text-xs text-tertiary pl-6">{ban.message}</p>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </Section>
148
+ )}
149
+
150
+ {/* Empty sections message */}
151
+ {isPartialContract(contract) && (
152
+ <div className="text-xs text-quaternary p-3 bg-[--bg-hover] rounded">
153
+ Tip: Add more contract fields to help AI agents better understand this component.
154
+ </div>
155
+ )}
156
+ </div>
157
+ );
158
+ });
159
+
160
+ // Helper component for consistent section styling
161
+ interface SectionProps {
162
+ title: string;
163
+ subtitle?: string;
164
+ children: React.ReactNode;
165
+ }
166
+
167
+ function Section({ title, subtitle, children }: SectionProps) {
168
+ return (
169
+ <div>
170
+ <h3 className="text-xs font-semibold text-primary mb-2 flex items-center gap-2">
171
+ {title}
172
+ {subtitle && (
173
+ <span className="font-normal text-quaternary">({subtitle})</span>
174
+ )}
175
+ </h3>
176
+ {children}
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // Check if contract is empty or has no meaningful content
182
+ function isContractEmpty(contract: SegmentContract): boolean {
183
+ return (
184
+ (!contract.propsSummary || contract.propsSummary.length === 0) &&
185
+ (!contract.scenarioTags || contract.scenarioTags.length === 0) &&
186
+ (!contract.a11yRules || contract.a11yRules.length === 0) &&
187
+ (!contract.bans || contract.bans.length === 0)
188
+ );
189
+ }
190
+
191
+ // Check if contract has some but not all fields
192
+ function isPartialContract(contract: SegmentContract): boolean {
193
+ const fieldCount = [
194
+ contract.propsSummary?.length ?? 0,
195
+ contract.scenarioTags?.length ?? 0,
196
+ contract.a11yRules?.length ?? 0,
197
+ contract.bans?.length ?? 0,
198
+ ].filter(n => n > 0).length;
199
+
200
+ return fieldCount > 0 && fieldCount < 3;
201
+ }
202
+
203
+ // Parse a prop summary string like "name: type - description" or "name: type (default: value)"
204
+ function parsePropSummary(prop: string): { name: string; type: string; description: string } {
205
+ // Try to match "name: type - description" format
206
+ const colonIndex = prop.indexOf(':');
207
+ if (colonIndex === -1) {
208
+ return { name: prop, type: '', description: '' };
209
+ }
210
+
211
+ const name = prop.slice(0, colonIndex).trim();
212
+ const rest = prop.slice(colonIndex + 1).trim();
213
+
214
+ // Check for " - description" pattern
215
+ const dashIndex = rest.indexOf(' - ');
216
+ if (dashIndex !== -1) {
217
+ const type = rest.slice(0, dashIndex).trim();
218
+ const description = rest.slice(dashIndex + 3).trim();
219
+ return { name, type, description };
220
+ }
221
+
222
+ // Check for "(default: value)" or "(default: value) description" pattern
223
+ const defaultMatch = rest.match(/^([^(]+)(\(default:\s*[^)]+\))(.*)$/);
224
+ if (defaultMatch) {
225
+ const type = defaultMatch[1].trim();
226
+ const defaultInfo = defaultMatch[2];
227
+ const extraDesc = defaultMatch[3].trim();
228
+ const description = extraDesc ? `${defaultInfo} ${extraDesc}` : defaultInfo;
229
+ return { name, type, description };
230
+ }
231
+
232
+ // Just type, no description
233
+ return { name, type: rest, description: '' };
234
+ }
@@ -0,0 +1,85 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from 'react';
2
+ import { ErrorIcon, RefreshIcon } from './Icons.js';
3
+
4
+ interface ErrorBoundaryProps {
5
+ children: ReactNode;
6
+ componentName?: string;
7
+ onRetry?: () => void;
8
+ }
9
+
10
+ interface ErrorBoundaryState {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ errorInfo: ErrorInfo | null;
14
+ }
15
+
16
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
17
+ constructor(props: ErrorBoundaryProps) {
18
+ super(props);
19
+ this.state = { hasError: false, error: null, errorInfo: null };
20
+ }
21
+
22
+ static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
23
+ return { hasError: true, error };
24
+ }
25
+
26
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
27
+ // Log error to console with full stack trace
28
+ console.error('Component Error:', error);
29
+ console.error('Component Stack:', errorInfo.componentStack);
30
+
31
+ this.setState({ errorInfo });
32
+ }
33
+
34
+ handleRetry = () => {
35
+ this.setState({ hasError: false, error: null, errorInfo: null });
36
+ this.props.onRetry?.();
37
+ };
38
+
39
+ render() {
40
+ if (this.state.hasError) {
41
+ const { componentName } = this.props;
42
+ const { error, errorInfo } = this.state;
43
+
44
+ return (
45
+ <div className="p-6 rounded-xl border-2 border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-950">
46
+ <div className="flex items-start gap-3">
47
+ <ErrorIcon className="w-6 h-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
48
+ <div className="flex-1 min-w-0">
49
+ <h3 className="text-sm font-semibold text-red-900 dark:text-red-100 mb-1">
50
+ {componentName ? `Error rendering ${componentName}` : 'Component Error'}
51
+ </h3>
52
+
53
+ {error && (
54
+ <p className="text-sm text-red-800 dark:text-red-200 mb-3 font-mono">
55
+ {error.message}
56
+ </p>
57
+ )}
58
+
59
+ {errorInfo?.componentStack && (
60
+ <details className="mb-4">
61
+ <summary className="text-xs text-red-700 dark:text-red-300 cursor-pointer hover:text-red-900 dark:hover:text-red-100 transition-colors">
62
+ Show stack trace
63
+ </summary>
64
+ <pre className="mt-2 p-3 text-[10px] font-mono bg-red-100 dark:bg-red-900 rounded-lg overflow-x-auto text-red-800 dark:text-red-200 whitespace-pre-wrap">
65
+ {error?.stack || errorInfo.componentStack}
66
+ </pre>
67
+ </details>
68
+ )}
69
+
70
+ <button
71
+ onClick={this.handleRetry}
72
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-red-600 text-white hover:bg-red-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
73
+ >
74
+ <RefreshIcon className="w-3.5 h-3.5" />
75
+ Retry
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ return this.props.children;
84
+ }
85
+ }
@@ -0,0 +1,231 @@
1
+ import { useRef, useEffect, useState, useMemo, useCallback } from "react";
2
+ import { FigmaIcon } from "./Icons.js";
3
+
4
+ interface FigmaEmbedProps {
5
+ /** Current Figma URL to display */
6
+ figmaUrl: string;
7
+ /** All Figma URLs for the current component (for preloading) */
8
+ allFigmaUrls?: string[];
9
+ zoom?: number;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ }
13
+
14
+ interface ParsedFigmaUrl {
15
+ fileKey: string;
16
+ nodeId?: string;
17
+ fullUrl: string;
18
+ }
19
+
20
+ /**
21
+ * Parse a Figma URL to extract file key and node ID.
22
+ */
23
+ function parseFigmaUrl(figmaUrl: string): ParsedFigmaUrl | null {
24
+ try {
25
+ const urlPattern = /figma\.com\/(?:file|design)\/([^\/]+)\/[^?]*(?:\?.*node-id=([^&]+))?/i;
26
+ const match = figmaUrl.match(urlPattern);
27
+
28
+ if (!match) return null;
29
+
30
+ const fileKey = match[1];
31
+ const nodeId = match[2] ? decodeURIComponent(match[2]) : undefined;
32
+
33
+ return { fileKey, nodeId, fullUrl: figmaUrl };
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Build a Figma embed URL.
41
+ */
42
+ function buildEmbedUrl(fileKey: string, nodeId?: string): string {
43
+ let embedUrl = `https://embed.figma.com/design/${fileKey}?embed-host=segments`;
44
+
45
+ if (nodeId) {
46
+ const embedNodeId = nodeId.replace(/:/g, "-");
47
+ embedUrl += `&node-id=${embedNodeId}`;
48
+ }
49
+
50
+ embedUrl += "&hide-ui=1";
51
+ return embedUrl;
52
+ }
53
+
54
+ /**
55
+ * Get a unique key for a Figma URL (used for iframe identification)
56
+ */
57
+ function getUrlKey(figmaUrl: string): string {
58
+ const parsed = parseFigmaUrl(figmaUrl);
59
+ if (!parsed) return figmaUrl;
60
+ return `${parsed.fileKey}:${parsed.nodeId || "default"}`;
61
+ }
62
+
63
+ /**
64
+ * FigmaEmbed component with iframe pooling for instant variant switching.
65
+ *
66
+ * Strategy: Pre-load iframes for ALL variants of the current component.
67
+ * When switching variants, we just show/hide the appropriate iframe.
68
+ * This makes switching instantaneous since iframes are already loaded.
69
+ *
70
+ * The design embed doesn't support postMessage navigation (only prototype
71
+ * embeds do), so this pooling approach is the only way to achieve instant
72
+ * switching.
73
+ */
74
+ export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, style }: FigmaEmbedProps) {
75
+ // Track loaded iframes
76
+ const [loadedUrls, setLoadedUrls] = useState<Set<string>>(new Set());
77
+ const [error, setError] = useState<string | null>(null);
78
+
79
+ // Parse the current URL
80
+ const currentParsed = useMemo(() => parseFigmaUrl(figmaUrl), [figmaUrl]);
81
+ const currentKey = useMemo(() => getUrlKey(figmaUrl), [figmaUrl]);
82
+
83
+ // Get all URLs to preload (deduplicated)
84
+ const urlsToPreload = useMemo(() => {
85
+ const urls = allFigmaUrls && allFigmaUrls.length > 0 ? allFigmaUrls : [figmaUrl];
86
+ const uniqueUrls = new Map<string, string>();
87
+
88
+ for (const url of urls) {
89
+ const key = getUrlKey(url);
90
+ if (!uniqueUrls.has(key)) {
91
+ uniqueUrls.set(key, url);
92
+ }
93
+ }
94
+
95
+ return Array.from(uniqueUrls.entries()).map(([key, url]) => ({
96
+ key,
97
+ url,
98
+ parsed: parseFigmaUrl(url),
99
+ }));
100
+ }, [allFigmaUrls, figmaUrl]);
101
+
102
+ // Handle iframe load
103
+ const handleIframeLoad = useCallback((urlKey: string) => {
104
+ setLoadedUrls(prev => {
105
+ const next = new Set(prev);
106
+ next.add(urlKey);
107
+ return next;
108
+ });
109
+ }, []);
110
+
111
+ // Calculate zoom transform
112
+ const zoomStyle: React.CSSProperties = zoom !== 100
113
+ ? {
114
+ transform: `scale(${zoom / 100})`,
115
+ transformOrigin: "center",
116
+ width: `${100 / (zoom / 100)}%`,
117
+ height: `${100 / (zoom / 100)}%`,
118
+ }
119
+ : {};
120
+
121
+ // Check if current URL is loaded
122
+ const isCurrentLoaded = loadedUrls.has(currentKey);
123
+
124
+ // If we can't parse the URL, show error
125
+ if (!currentParsed) {
126
+ return (
127
+ <div className={className} style={{ ...style, display: "flex", alignItems: "center", justifyContent: "center" }}>
128
+ <div className="flex flex-col items-center gap-2 text-tertiary p-4 text-center">
129
+ <FigmaIcon className="w-6 h-6" />
130
+ <span className="text-xs">Unable to embed Figma design</span>
131
+ <button
132
+ onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
133
+ className="text-xs text-[--color-accent] hover:underline"
134
+ >
135
+ Open in Figma
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <div className={className} style={{ ...style, position: "relative", overflow: "hidden" }}>
144
+ {/* Loading overlay - shows while current iframe is loading */}
145
+ {!isCurrentLoaded && (
146
+ <div className="absolute inset-0 flex items-center justify-center bg-[--bg-secondary] z-20">
147
+ <div className="flex flex-col items-center gap-2">
148
+ <FigmaIcon className="w-5 h-5 text-tertiary animate-pulse" />
149
+ <span className="text-xs text-tertiary">Loading Figma...</span>
150
+ </div>
151
+ </div>
152
+ )}
153
+
154
+ {/* Error overlay */}
155
+ {error && (
156
+ <div className="absolute inset-0 flex items-center justify-center bg-[--bg-secondary] z-20">
157
+ <div className="flex flex-col items-center gap-2 text-tertiary">
158
+ <FigmaIcon className="w-6 h-6" />
159
+ <span className="text-xs">{error}</span>
160
+ </div>
161
+ </div>
162
+ )}
163
+
164
+ {/*
165
+ Iframe pool: Pre-load all variant URLs as hidden iframes.
166
+ Only the current variant's iframe is visible (opacity: 1, z-index: 10).
167
+ Others are hidden (opacity: 0, z-index: 1) but stay loaded.
168
+
169
+ This approach works because:
170
+ 1. Figma iframes stay active even when hidden
171
+ 2. Switching just changes CSS visibility
172
+ 3. No network requests when switching variants
173
+ */}
174
+ {urlsToPreload.map(({ key, parsed }) => {
175
+ if (!parsed) return null;
176
+
177
+ const embedUrl = buildEmbedUrl(parsed.fileKey, parsed.nodeId);
178
+ const isActive = key === currentKey;
179
+ const isLoaded = loadedUrls.has(key);
180
+
181
+ return (
182
+ <iframe
183
+ key={key}
184
+ src={embedUrl}
185
+ className="absolute inset-0 w-full h-full border-0 transition-opacity duration-150"
186
+ style={{
187
+ ...zoomStyle,
188
+ opacity: isActive && isLoaded ? 1 : 0,
189
+ zIndex: isActive ? 10 : 1,
190
+ pointerEvents: isActive ? "auto" : "none",
191
+ }}
192
+ onLoad={() => handleIframeLoad(key)}
193
+ onError={() => setError("Failed to load Figma embed")}
194
+ allowFullScreen
195
+ />
196
+ );
197
+ })}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Hook to collect all Figma URLs from a segment's variants.
204
+ * This enables the FigmaEmbed to preload all variant iframes.
205
+ */
206
+ export function useAllFigmaUrls(
207
+ segment: { meta: { figma?: string }; variants?: Array<{ figma?: string }> } | undefined
208
+ ): string[] {
209
+ return useMemo(() => {
210
+ if (!segment) return [];
211
+
212
+ const urls: string[] = [];
213
+
214
+ // Add meta-level Figma URL
215
+ if (segment.meta.figma) {
216
+ urls.push(segment.meta.figma);
217
+ }
218
+
219
+ // Add variant-level Figma URLs
220
+ if (segment.variants) {
221
+ for (const variant of segment.variants) {
222
+ if (variant.figma) {
223
+ urls.push(variant.figma);
224
+ }
225
+ }
226
+ }
227
+
228
+ // Deduplicate
229
+ return [...new Set(urls)];
230
+ }, [segment]);
231
+ }