@fragments-sdk/cli 0.10.1 → 0.12.1

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 (223) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +292 -367
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
  5. package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
  6. package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
  7. package/dist/chunk-GVDSFQ4E.js.map +1 -0
  8. package/dist/chunk-JJ2VRTBU.js +626 -0
  9. package/dist/chunk-JJ2VRTBU.js.map +1 -0
  10. package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
  11. package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
  12. package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
  13. package/dist/chunk-OQKMEFOS.js.map +1 -0
  14. package/dist/chunk-SXTKFDCR.js +104 -0
  15. package/dist/chunk-SXTKFDCR.js.map +1 -0
  16. package/dist/chunk-T5OMVL7E.js +443 -0
  17. package/dist/chunk-T5OMVL7E.js.map +1 -0
  18. package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
  19. package/dist/chunk-TPWGL2XS.js.map +1 -0
  20. package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
  21. package/dist/chunk-WFS63PCW.js.map +1 -0
  22. package/dist/core/index.js +9 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-ZSX3NRCZ.js +636 -0
  28. package/dist/init-ZSX3NRCZ.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
  31. package/dist/scan-generate-SYU4PYZD.js +1115 -0
  32. package/dist/scan-generate-SYU4PYZD.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
  34. package/dist/snapshot-XOISO2IS.js +139 -0
  35. package/dist/snapshot-XOISO2IS.js.map +1 -0
  36. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
  37. package/dist/static-viewer-5GXH2MGE.js.map +1 -0
  38. package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
  39. package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
  40. package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
  41. package/dist/viewer-7ZEAFBVN.js.map +1 -0
  42. package/package.json +6 -14
  43. package/src/ai-client.ts +156 -0
  44. package/src/bin.ts +74 -2
  45. package/src/build.ts +95 -33
  46. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  47. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  48. package/src/commands/enhance.ts +11 -35
  49. package/src/commands/init.ts +296 -193
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/snapshot.ts +197 -0
  54. package/src/commands/sync.ts +357 -0
  55. package/src/commands/validate.ts +43 -1
  56. package/src/core/component-extractor.test.ts +282 -0
  57. package/src/core/component-extractor.ts +1030 -0
  58. package/src/core/discovery.ts +93 -7
  59. package/src/service/enhance/props-extractor.ts +235 -13
  60. package/src/validators.ts +236 -0
  61. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  62. package/src/viewer/server.ts +37 -22
  63. package/src/viewer/vite-plugin.ts +25 -9
  64. package/dist/chunk-5G3VZH43.js.map +0 -1
  65. package/dist/chunk-OQO55NKV.js.map +0 -1
  66. package/dist/chunk-WXSR2II7.js.map +0 -1
  67. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  68. package/dist/init-NDQXUWDU.js +0 -796
  69. package/dist/init-NDQXUWDU.js.map +0 -1
  70. package/dist/scan-generate-SJAN5MVI.js +0 -691
  71. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  72. package/dist/viewer-DNMNC5VS.js.map +0 -1
  73. package/src/ai.ts +0 -266
  74. package/src/commands/init-framework.ts +0 -414
  75. package/src/mcp/bin.ts +0 -36
  76. package/src/migrate/bin.ts +0 -114
  77. package/src/theme/index.ts +0 -77
  78. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  79. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  80. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  81. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  82. package/src/viewer/assets/fragments-logo.ts +0 -4
  83. package/src/viewer/assets/fragments_logo.png +0 -0
  84. package/src/viewer/bin.ts +0 -86
  85. package/src/viewer/cli/health.ts +0 -256
  86. package/src/viewer/cli/index.ts +0 -33
  87. package/src/viewer/cli/scan.ts +0 -124
  88. package/src/viewer/cli/utils.ts +0 -174
  89. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  90. package/src/viewer/components/ActionCapture.tsx +0 -172
  91. package/src/viewer/components/ActionsPanel.tsx +0 -332
  92. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  93. package/src/viewer/components/App.tsx +0 -582
  94. package/src/viewer/components/BottomPanel.tsx +0 -288
  95. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  96. package/src/viewer/components/CodePanel.tsx +0 -118
  97. package/src/viewer/components/CommandPalette.tsx +0 -392
  98. package/src/viewer/components/ComponentDocView.tsx +0 -164
  99. package/src/viewer/components/ComponentGraph.tsx +0 -380
  100. package/src/viewer/components/ComponentHeader.tsx +0 -88
  101. package/src/viewer/components/ContractPanel.tsx +0 -241
  102. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  103. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  104. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  105. package/src/viewer/components/FragmentEditor.tsx +0 -525
  106. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  107. package/src/viewer/components/HeaderSearch.tsx +0 -24
  108. package/src/viewer/components/HealthDashboard.tsx +0 -441
  109. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  110. package/src/viewer/components/Icons.tsx +0 -479
  111. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  112. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  113. package/src/viewer/components/IsolatedRender.tsx +0 -113
  114. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  115. package/src/viewer/components/LandingPage.tsx +0 -421
  116. package/src/viewer/components/Layout.tsx +0 -27
  117. package/src/viewer/components/LeftSidebar.tsx +0 -472
  118. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  119. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  120. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  121. package/src/viewer/components/PanelShell.tsx +0 -161
  122. package/src/viewer/components/PerformancePanel.tsx +0 -304
  123. package/src/viewer/components/PreviewArea.tsx +0 -472
  124. package/src/viewer/components/PreviewAside.tsx +0 -168
  125. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  126. package/src/viewer/components/PreviewPane.tsx +0 -149
  127. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  128. package/src/viewer/components/PropsEditor.tsx +0 -506
  129. package/src/viewer/components/PropsTable.tsx +0 -111
  130. package/src/viewer/components/RelationsSection.tsx +0 -88
  131. package/src/viewer/components/ResizablePanel.tsx +0 -271
  132. package/src/viewer/components/RightSidebar.tsx +0 -102
  133. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  134. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  135. package/src/viewer/components/Sidebar.tsx +0 -169
  136. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  137. package/src/viewer/components/ThemeProvider.tsx +0 -42
  138. package/src/viewer/components/Toast.tsx +0 -3
  139. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  140. package/src/viewer/components/TopToolbar.tsx +0 -159
  141. package/src/viewer/components/UsageSection.tsx +0 -95
  142. package/src/viewer/components/VariantMatrix.tsx +0 -388
  143. package/src/viewer/components/VariantRenderer.tsx +0 -131
  144. package/src/viewer/components/VariantTabs.tsx +0 -40
  145. package/src/viewer/components/ViewerHeader.tsx +0 -69
  146. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  147. package/src/viewer/components/ViewportSelector.tsx +0 -172
  148. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  149. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  150. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  151. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  152. package/src/viewer/components/viewer-utils.ts +0 -16
  153. package/src/viewer/composition-renderer.ts +0 -381
  154. package/src/viewer/constants/index.ts +0 -1
  155. package/src/viewer/constants/ui.ts +0 -166
  156. package/src/viewer/entry.tsx +0 -335
  157. package/src/viewer/hooks/index.ts +0 -2
  158. package/src/viewer/hooks/useA11yCache.ts +0 -383
  159. package/src/viewer/hooks/useA11yService.ts +0 -364
  160. package/src/viewer/hooks/useActions.ts +0 -138
  161. package/src/viewer/hooks/useAppState.ts +0 -147
  162. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  163. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  164. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  165. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  166. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  167. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  168. package/src/viewer/hooks/useUrlState.ts +0 -318
  169. package/src/viewer/hooks/useViewSettings.ts +0 -111
  170. package/src/viewer/index.html +0 -28
  171. package/src/viewer/intelligence/healthReport.ts +0 -505
  172. package/src/viewer/intelligence/styleDrift.ts +0 -340
  173. package/src/viewer/intelligence/usageScanner.ts +0 -309
  174. package/src/viewer/jsx-parser.ts +0 -486
  175. package/src/viewer/preview-frame-entry.tsx +0 -25
  176. package/src/viewer/preview-frame.html +0 -125
  177. package/src/viewer/public/favicon.ico +0 -0
  178. package/src/viewer/render-template.html +0 -68
  179. package/src/viewer/styles/globals.css +0 -278
  180. package/src/viewer/types/a11y.ts +0 -197
  181. package/src/viewer/utils/a11y-fixes.ts +0 -509
  182. package/src/viewer/utils/actionExport.ts +0 -372
  183. package/src/viewer/utils/colorSchemes.ts +0 -201
  184. package/src/viewer/utils/detectRelationships.ts +0 -256
  185. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  186. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  187. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  188. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  189. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  190. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  191. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  192. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  193. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  194. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  195. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  196. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  197. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  198. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  199. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  200. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  201. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  202. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  203. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  204. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  205. package/src/viewer/vendor/shared/src/index.ts +0 -34
  206. package/src/viewer/vendor/shared/src/types.ts +0 -53
  207. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  208. package/src/viewer/webmcp/analytics.ts +0 -165
  209. package/src/viewer/webmcp/index.ts +0 -3
  210. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  211. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  212. package/src/viewer/webmcp/scan-utils.ts +0 -135
  213. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  214. package/src/viewer/webmcp/viewer-state.ts +0 -45
  215. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  216. /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
  217. /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
  218. /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
  219. /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
  220. /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
  221. /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
  222. /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
  223. /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
@@ -1,241 +0,0 @@
1
- /**
2
- * ContractPanel component - displays FragmentContract metadata for AI agents.
3
- * Shows propsSummary, scenarioTags, bans, and a11yRules.
4
- */
5
-
6
- import { memo } from 'react';
7
- import type { FragmentContract } from '../../core/index.js';
8
- import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge, Box } from '@fragments-sdk/ui';
9
-
10
- interface ContractPanelProps {
11
- contract?: FragmentContract;
12
- componentName: string;
13
- }
14
-
15
- export const ContractPanel = memo(function ContractPanel({
16
- contract,
17
- componentName,
18
- }: ContractPanelProps) {
19
- // Empty state when no contract metadata
20
- if (!contract || isContractEmpty(contract)) {
21
- return (
22
- <EmptyState>
23
- <EmptyState.Icon>
24
- <svg
25
- style={{ width: '48px', height: '48px', opacity: 0.5 }}
26
- fill="none"
27
- viewBox="0 0 24 24"
28
- stroke="currentColor"
29
- >
30
- <path
31
- strokeLinecap="round"
32
- strokeLinejoin="round"
33
- strokeWidth={1.5}
34
- 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"
35
- />
36
- </svg>
37
- </EmptyState.Icon>
38
- <EmptyState.Title>No contract metadata</EmptyState.Title>
39
- <EmptyState.Description>
40
- Add a <Box as="code" padding="xs" background="secondary" rounded="sm">contract</Box> field to the fragment definition to enable AI agent features.
41
- </EmptyState.Description>
42
- <EmptyState.Actions>
43
- <CodeBlock
44
- code={`contract: {
45
- propsSummary: ['variant: primary|secondary'],
46
- scenarioTags: ['form.submit', 'action.primary'],
47
- a11yRules: ['A11Y_BTN_LABEL'],
48
- }`}
49
- language="typescript"
50
- compact
51
- />
52
- </EmptyState.Actions>
53
- </EmptyState>
54
- );
55
- }
56
-
57
- return (
58
- <Box padding="md" style={{ fontSize: '14px' }}>
59
- <Stack gap="lg">
60
- {/* Props Summary */}
61
- {contract.propsSummary && contract.propsSummary.length > 0 && (
62
- <Section title="Props Summary">
63
- <Card>
64
- <Card.Body>
65
- <table style={{ width: '100%', fontSize: '12px', borderCollapse: 'collapse' }}>
66
- <thead>
67
- <tr style={{ borderBottom: '1px solid var(--border)' }}>
68
- <th style={{ textAlign: 'left', padding: '8px 16px 8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Name</th>
69
- <th style={{ textAlign: 'left', padding: '8px 16px 8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Type</th>
70
- <th style={{ textAlign: 'left', padding: '8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Description</th>
71
- </tr>
72
- </thead>
73
- <tbody>
74
- {contract.propsSummary.map((prop, i) => {
75
- const parsed = parsePropSummary(prop);
76
- return (
77
- <tr key={i} style={{ borderBottom: i < contract.propsSummary!.length - 1 ? '1px solid var(--border)' : undefined }}>
78
- <td style={{ padding: '8px 16px 8px 0', verticalAlign: 'top' }}>
79
- <Text font="mono" size="xs" weight="medium">{parsed.name}</Text>
80
- </td>
81
- <td style={{ padding: '8px 16px 8px 0', verticalAlign: 'top' }}>
82
- <Badge size="sm">{parsed.type}</Badge>
83
- </td>
84
- <td style={{ padding: '8px 0', verticalAlign: 'top' }}>
85
- <Text size="xs" color="secondary">{parsed.description}</Text>
86
- </td>
87
- </tr>
88
- );
89
- })}
90
- </tbody>
91
- </table>
92
- </Card.Body>
93
- </Card>
94
- </Section>
95
- )}
96
-
97
- {/* Scenario Tags */}
98
- {contract.scenarioTags && contract.scenarioTags.length > 0 && (
99
- <Section title="Scenario Tags" subtitle="Used by fragments_suggest for AI queries">
100
- <Stack direction="row" gap="sm" style={{ flexWrap: 'wrap' }}>
101
- {contract.scenarioTags.map((tag, i) => (
102
- <Chip key={i}>{tag}</Chip>
103
- ))}
104
- </Stack>
105
- </Section>
106
- )}
107
-
108
- {/* Accessibility Rules */}
109
- {contract.a11yRules && contract.a11yRules.length > 0 && (
110
- <Section title="Accessibility Rules">
111
- <Stack gap="sm">
112
- {contract.a11yRules.map((rule, i) => (
113
- <Box key={i} padding="sm" background="secondary" rounded="sm">
114
- <Stack
115
- direction="row"
116
- align="center"
117
- gap="sm"
118
- >
119
- <span style={{ color: '#22c55e' }}>
120
- <svg style={{ width: '16px', height: '16px' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
121
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
122
- </svg>
123
- </span>
124
- <Text font="mono" size="xs" color="secondary">{rule}</Text>
125
- </Stack>
126
- </Box>
127
- ))}
128
- </Stack>
129
- </Section>
130
- )}
131
-
132
- {/* Bans */}
133
- {contract.bans && contract.bans.length > 0 && (
134
- <Section title="Usage Bans" subtitle="Patterns to avoid">
135
- <Stack gap="sm">
136
- {contract.bans.map((ban, i) => (
137
- <Alert key={i} severity="warning">
138
- <Alert.Body>
139
- <Alert.Title>
140
- <Text font="mono" size="xs">{ban.pattern}</Text>
141
- </Alert.Title>
142
- <Alert.Content>
143
- <Text size="xs" color="tertiary">{ban.message}</Text>
144
- </Alert.Content>
145
- </Alert.Body>
146
- </Alert>
147
- ))}
148
- </Stack>
149
- </Section>
150
- )}
151
-
152
- {/* Empty sections message */}
153
- {isPartialContract(contract) && (
154
- <Alert severity="info">
155
- <Alert.Body>
156
- <Alert.Content>
157
- Tip: Add more contract fields to help AI agents better understand this component.
158
- </Alert.Content>
159
- </Alert.Body>
160
- </Alert>
161
- )}
162
- </Stack>
163
- </Box>
164
- );
165
- });
166
-
167
- // Helper component for consistent section styling
168
- interface SectionProps {
169
- title: string;
170
- subtitle?: string;
171
- children: React.ReactNode;
172
- }
173
-
174
- function Section({ title, subtitle, children }: SectionProps) {
175
- return (
176
- <div>
177
- <Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
178
- <Text as="h3" size="xs" weight="semibold">{title}</Text>
179
- {subtitle && (
180
- <Text size="xs" color="tertiary">({subtitle})</Text>
181
- )}
182
- </Stack>
183
- {children}
184
- </div>
185
- );
186
- }
187
-
188
- // Check if contract is empty or has no meaningful content
189
- function isContractEmpty(contract: FragmentContract): boolean {
190
- return (
191
- (!contract.propsSummary || contract.propsSummary.length === 0) &&
192
- (!contract.scenarioTags || contract.scenarioTags.length === 0) &&
193
- (!contract.a11yRules || contract.a11yRules.length === 0) &&
194
- (!contract.bans || contract.bans.length === 0)
195
- );
196
- }
197
-
198
- // Check if contract has some but not all fields
199
- function isPartialContract(contract: FragmentContract): boolean {
200
- const fieldCount = [
201
- contract.propsSummary?.length ?? 0,
202
- contract.scenarioTags?.length ?? 0,
203
- contract.a11yRules?.length ?? 0,
204
- contract.bans?.length ?? 0,
205
- ].filter(n => n > 0).length;
206
-
207
- return fieldCount > 0 && fieldCount < 3;
208
- }
209
-
210
- // Parse a prop summary string like "name: type - description" or "name: type (default: value)"
211
- function parsePropSummary(prop: string): { name: string; type: string; description: string } {
212
- // Try to match "name: type - description" format
213
- const colonIndex = prop.indexOf(':');
214
- if (colonIndex === -1) {
215
- return { name: prop, type: '', description: '' };
216
- }
217
-
218
- const name = prop.slice(0, colonIndex).trim();
219
- const rest = prop.slice(colonIndex + 1).trim();
220
-
221
- // Check for " - description" pattern
222
- const dashIndex = rest.indexOf(' - ');
223
- if (dashIndex !== -1) {
224
- const type = rest.slice(0, dashIndex).trim();
225
- const description = rest.slice(dashIndex + 3).trim();
226
- return { name, type, description };
227
- }
228
-
229
- // Check for "(default: value)" or "(default: value) description" pattern
230
- const defaultMatch = rest.match(/^([^(]+)(\(default:\s*[^)]+\))(.*)$/);
231
- if (defaultMatch) {
232
- const type = defaultMatch[1].trim();
233
- const defaultInfo = defaultMatch[2];
234
- const extraDesc = defaultMatch[3].trim();
235
- const description = extraDesc ? `${defaultInfo} ${extraDesc}` : defaultInfo;
236
- return { name, type, description };
237
- }
238
-
239
- // Just type, no description
240
- return { name, type: rest, description: '' };
241
- }
@@ -1,54 +0,0 @@
1
- import { Stack, Text, Alert } from "@fragments-sdk/ui";
2
-
3
- interface EmptyVariantMessageProps {
4
- reason: string;
5
- variantName: string;
6
- hint?: string;
7
- }
8
-
9
- export function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
10
- return (
11
- <Alert variant="warning">
12
- <Alert.Body>
13
- <Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
14
- <Alert.Content>
15
- <Stack direction="column" gap="sm">
16
- <Text size="xs" color="secondary">
17
- {reason}
18
- </Text>
19
- {hint && (
20
- <Text size="xs" color="tertiary">
21
- <Text as="span" size="xs" weight="semibold">
22
- Tip:
23
- </Text>{" "}
24
- {hint}
25
- </Text>
26
- )}
27
- <div>
28
- <Text size="xs" color="tertiary" weight="semibold">
29
- Common causes:
30
- </Text>
31
- <ul style={{ marginTop: "4px", marginLeft: "16px", listStyleType: "disc" }}>
32
- <li>
33
- <Text size="xs" color="secondary">
34
- Component requires props that weren't provided
35
- </Text>
36
- </li>
37
- <li>
38
- <Text size="xs" color="secondary">
39
- Component renders conditionally and conditions aren't met
40
- </Text>
41
- </li>
42
- <li>
43
- <Text size="xs" color="secondary">
44
- Story args reference variables that don't exist in this context
45
- </Text>
46
- </li>
47
- </ul>
48
- </div>
49
- </Stack>
50
- </Alert.Content>
51
- </Alert.Body>
52
- </Alert>
53
- );
54
- }
@@ -1,97 +0,0 @@
1
- import { Component, type ReactNode, type ErrorInfo } from 'react';
2
- import { Button, Alert, CodeBlock, Collapsible, Stack, Text } from '@fragments-sdk/ui';
3
- import { ErrorIcon, RefreshIcon } from './Icons.js';
4
-
5
- interface ErrorBoundaryProps {
6
- children: ReactNode;
7
- componentName?: string;
8
- fallback?: ReactNode;
9
- onRetry?: () => void;
10
- }
11
-
12
- interface ErrorBoundaryState {
13
- hasError: boolean;
14
- error: Error | null;
15
- errorInfo: ErrorInfo | null;
16
- }
17
-
18
- export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
19
- constructor(props: ErrorBoundaryProps) {
20
- super(props);
21
- this.state = { hasError: false, error: null, errorInfo: null };
22
- }
23
-
24
- static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
25
- return { hasError: true, error };
26
- }
27
-
28
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
29
- console.error('Component Error:', error);
30
- console.error('Component Stack:', errorInfo.componentStack);
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
- if (this.props.fallback) {
42
- return this.props.fallback;
43
- }
44
-
45
- const { componentName } = this.props;
46
- const { error, errorInfo } = this.state;
47
-
48
- return (
49
- <Alert variant="error">
50
- <Alert.Icon>
51
- <ErrorIcon style={{ width: '24px', height: '24px' }} />
52
- </Alert.Icon>
53
- <Alert.Body>
54
- <Alert.Title>
55
- {componentName ? `Error rendering ${componentName}` : 'Component Error'}
56
- </Alert.Title>
57
- <Alert.Content>
58
- <Stack direction="column" gap="sm">
59
- {error && (
60
- <Text size="sm" font="mono" style={{ color: '#dc2626' }}>
61
- {error.message}
62
- </Text>
63
- )}
64
-
65
- {errorInfo?.componentStack && (
66
- <Collapsible>
67
- <Collapsible.Trigger>
68
- <Text size="xs" color="tertiary" style={{ cursor: 'pointer' }}>
69
- Show stack trace
70
- </Text>
71
- </Collapsible.Trigger>
72
- <Collapsible.Content>
73
- <div style={{ marginTop: '8px' }}>
74
- <CodeBlock language="plaintext">
75
- {error?.stack || errorInfo.componentStack}
76
- </CodeBlock>
77
- </div>
78
- </Collapsible.Content>
79
- </Collapsible>
80
- )}
81
-
82
- <div>
83
- <Button variant="danger" size="sm" onClick={this.handleRetry}>
84
- <RefreshIcon style={{ width: '14px', height: '14px' }} />
85
- Retry
86
- </Button>
87
- </div>
88
- </Stack>
89
- </Alert.Content>
90
- </Alert.Body>
91
- </Alert>
92
- );
93
- }
94
-
95
- return this.props.children;
96
- }
97
- }
@@ -1,238 +0,0 @@
1
- import { useRef, useEffect, useState, useMemo, useCallback } from "react";
2
- import { FigmaIcon } from "./Icons.js";
3
- import { Stack, Text, Button } from '@fragments-sdk/ui';
4
-
5
- interface FigmaEmbedProps {
6
- /** Current Figma URL to display */
7
- figmaUrl: string;
8
- /** All Figma URLs for the current component (for preloading) */
9
- allFigmaUrls?: string[];
10
- zoom?: number;
11
- className?: string;
12
- style?: React.CSSProperties;
13
- }
14
-
15
- interface ParsedFigmaUrl {
16
- fileKey: string;
17
- nodeId?: string;
18
- fullUrl: string;
19
- }
20
-
21
- /**
22
- * Parse a Figma URL to extract file key and node ID.
23
- */
24
- function parseFigmaUrl(figmaUrl: string): ParsedFigmaUrl | null {
25
- try {
26
- const urlPattern = /figma\.com\/(?:file|design)\/([^/]+)\/[^?]*(?:\?.*node-id=([^&]+))?/i;
27
- const match = figmaUrl.match(urlPattern);
28
-
29
- if (!match) return null;
30
-
31
- const fileKey = match[1];
32
- const nodeId = match[2] ? decodeURIComponent(match[2]) : undefined;
33
-
34
- return { fileKey, nodeId, fullUrl: figmaUrl };
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- /**
41
- * Build a Figma embed URL.
42
- */
43
- function buildEmbedUrl(fileKey: string, nodeId?: string): string {
44
- let embedUrl = `https://embed.figma.com/design/${fileKey}?embed-host=fragments`;
45
-
46
- if (nodeId) {
47
- const embedNodeId = nodeId.replace(/:/g, "-");
48
- embedUrl += `&node-id=${embedNodeId}`;
49
- }
50
-
51
- embedUrl += "&hide-ui=1";
52
- return embedUrl;
53
- }
54
-
55
- /**
56
- * Get a unique key for a Figma URL (used for iframe identification)
57
- */
58
- function getUrlKey(figmaUrl: string): string {
59
- const parsed = parseFigmaUrl(figmaUrl);
60
- if (!parsed) return figmaUrl;
61
- return `${parsed.fileKey}:${parsed.nodeId || "default"}`;
62
- }
63
-
64
- /**
65
- * FigmaEmbed component with iframe pooling for instant variant switching.
66
- *
67
- * Strategy: Pre-load iframes for ALL variants of the current component.
68
- * When switching variants, we just show/hide the appropriate iframe.
69
- * This makes switching instantaneous since iframes are already loaded.
70
- *
71
- * The design embed doesn't support postMessage navigation (only prototype
72
- * embeds do), so this pooling approach is the only way to achieve instant
73
- * switching.
74
- */
75
- export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, style }: FigmaEmbedProps) {
76
- // Track loaded iframes
77
- const [loadedUrls, setLoadedUrls] = useState<Set<string>>(new Set());
78
- const [error, setError] = useState<string | null>(null);
79
-
80
- // Parse the current URL
81
- const currentParsed = useMemo(() => parseFigmaUrl(figmaUrl), [figmaUrl]);
82
- const currentKey = useMemo(() => getUrlKey(figmaUrl), [figmaUrl]);
83
-
84
- // Get all URLs to preload (deduplicated)
85
- const urlsToPreload = useMemo(() => {
86
- const urls = allFigmaUrls && allFigmaUrls.length > 0 ? allFigmaUrls : [figmaUrl];
87
- const uniqueUrls = new Map<string, string>();
88
-
89
- for (const url of urls) {
90
- const key = getUrlKey(url);
91
- if (!uniqueUrls.has(key)) {
92
- uniqueUrls.set(key, url);
93
- }
94
- }
95
-
96
- return Array.from(uniqueUrls.entries()).map(([key, url]) => ({
97
- key,
98
- url,
99
- parsed: parseFigmaUrl(url),
100
- }));
101
- }, [allFigmaUrls, figmaUrl]);
102
-
103
- // Handle iframe load
104
- const handleIframeLoad = useCallback((urlKey: string) => {
105
- setLoadedUrls(prev => {
106
- const next = new Set(prev);
107
- next.add(urlKey);
108
- return next;
109
- });
110
- }, []);
111
-
112
- // Calculate zoom transform
113
- const zoomStyle: React.CSSProperties = zoom !== 100
114
- ? {
115
- transform: `scale(${zoom / 100})`,
116
- transformOrigin: "center",
117
- width: `${100 / (zoom / 100)}%`,
118
- height: `${100 / (zoom / 100)}%`,
119
- }
120
- : {};
121
-
122
- // Check if current URL is loaded
123
- const isCurrentLoaded = loadedUrls.has(currentKey);
124
-
125
- // If we can't parse the URL, show error
126
- if (!currentParsed) {
127
- return (
128
- <Stack className={className} align="center" justify="center" style={style}>
129
- <Stack direction="column" align="center" gap="sm" style={{ color: 'var(--text-tertiary)', padding: '16px', textAlign: 'center' }}>
130
- <FigmaIcon style={{ width: '24px', height: '24px' }} />
131
- <Text size="xs">Unable to embed Figma design</Text>
132
- <Button
133
- variant="ghost"
134
- size="sm"
135
- onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
136
- >
137
- Open in Figma
138
- </Button>
139
- </Stack>
140
- </Stack>
141
- );
142
- }
143
-
144
- return (
145
- <div className={className} style={{ ...style, position: "relative", overflow: "hidden" }}>
146
- {/* Loading overlay - shows while current iframe is loading */}
147
- {!isCurrentLoaded && (
148
- <Stack align="center" justify="center" style={{ position: 'absolute', inset: 0, backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
149
- <Stack direction="column" align="center" gap="sm">
150
- <FigmaIcon style={{ width: '20px', height: '20px', color: 'var(--text-tertiary)' }} />
151
- <Text size="xs" color="tertiary">Loading Figma...</Text>
152
- </Stack>
153
- </Stack>
154
- )}
155
-
156
- {/* Error overlay */}
157
- {error && (
158
- <Stack align="center" justify="center" style={{ position: 'absolute', inset: 0, backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
159
- <Stack direction="column" align="center" gap="sm" style={{ color: 'var(--text-tertiary)' }}>
160
- <FigmaIcon style={{ width: '24px', height: '24px' }} />
161
- <Text size="xs">{error}</Text>
162
- </Stack>
163
- </Stack>
164
- )}
165
-
166
- {/*
167
- Iframe pool: Pre-load all variant URLs as hidden iframes.
168
- Only the current variant's iframe is visible (opacity: 1, z-index: 10).
169
- Others are hidden (opacity: 0, z-index: 1) but stay loaded.
170
-
171
- This approach works because:
172
- 1. Figma iframes stay active even when hidden
173
- 2. Switching just changes CSS visibility
174
- 3. No network requests when switching variants
175
- */}
176
- {urlsToPreload.map(({ key, parsed }) => {
177
- if (!parsed) return null;
178
-
179
- const embedUrl = buildEmbedUrl(parsed.fileKey, parsed.nodeId);
180
- const isActive = key === currentKey;
181
- const isLoaded = loadedUrls.has(key);
182
-
183
- return (
184
- <iframe
185
- key={key}
186
- src={embedUrl}
187
- style={{
188
- position: 'absolute',
189
- inset: 0,
190
- width: '100%',
191
- height: '100%',
192
- border: 'none',
193
- transition: 'opacity 150ms',
194
- ...zoomStyle,
195
- opacity: isActive && isLoaded ? 1 : 0,
196
- zIndex: isActive ? 10 : 1,
197
- pointerEvents: isActive ? "auto" : "none",
198
- }}
199
- onLoad={() => handleIframeLoad(key)}
200
- onError={() => setError("Failed to load Figma embed")}
201
- allowFullScreen
202
- />
203
- );
204
- })}
205
- </div>
206
- );
207
- }
208
-
209
- /**
210
- * Hook to collect all Figma URLs from a fragment's variants.
211
- * This enables the FigmaEmbed to preload all variant iframes.
212
- */
213
- export function useAllFigmaUrls(
214
- fragment: { meta: { figma?: string }; variants?: Array<{ figma?: string }> } | undefined
215
- ): string[] {
216
- return useMemo(() => {
217
- if (!fragment) return [];
218
-
219
- const urls: string[] = [];
220
-
221
- // Add meta-level Figma URL
222
- if (fragment.meta.figma) {
223
- urls.push(fragment.meta.figma);
224
- }
225
-
226
- // Add variant-level Figma URLs
227
- if (fragment.variants) {
228
- for (const variant of fragment.variants) {
229
- if (variant.figma) {
230
- urls.push(variant.figma);
231
- }
232
- }
233
- }
234
-
235
- // Deduplicate
236
- return [...new Set(urls)];
237
- }, [fragment]);
238
- }