@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|