@fragments-sdk/cli 0.9.0 → 0.9.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 (123) hide show
  1. package/dist/bin.js +83 -33
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/build.ts +12 -2
  37. package/src/commands/build.ts +16 -2
  38. package/src/commands/generate.ts +383 -68
  39. package/src/commands/init.ts +9 -51
  40. package/src/core/config.ts +15 -2
  41. package/src/core/generators/typescript-extractor.ts +10 -0
  42. package/src/core/index.ts +15 -0
  43. package/src/core/schema.ts +10 -2
  44. package/src/core/storyFilters.test.ts +350 -0
  45. package/src/core/storyFilters.ts +253 -0
  46. package/src/core/types.ts +22 -0
  47. package/src/migrate/converter.ts +9 -1
  48. package/src/migrate/parser.ts +2 -0
  49. package/src/migrate/types.ts +2 -0
  50. package/src/setup.ts +69 -24
  51. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  52. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  53. package/src/viewer/components/ActionsPanel.tsx +31 -29
  54. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  55. package/src/viewer/components/App.tsx +187 -740
  56. package/src/viewer/components/BottomPanel.tsx +228 -132
  57. package/src/viewer/components/CodePanel.tsx +1 -1
  58. package/src/viewer/components/CommandPalette.tsx +7 -10
  59. package/src/viewer/components/ComponentDocView.tsx +164 -0
  60. package/src/viewer/components/ComponentGraph.tsx +111 -142
  61. package/src/viewer/components/ContractPanel.tsx +6 -6
  62. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  63. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  64. package/src/viewer/components/FragmentEditor.tsx +92 -115
  65. package/src/viewer/components/HeaderSearch.tsx +24 -0
  66. package/src/viewer/components/HealthDashboard.tsx +16 -2
  67. package/src/viewer/components/Icons.tsx +9 -0
  68. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  69. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  70. package/src/viewer/components/LandingPage.tsx +3 -3
  71. package/src/viewer/components/LeftSidebar.tsx +141 -63
  72. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  73. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  74. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  75. package/src/viewer/components/PanelShell.tsx +161 -0
  76. package/src/viewer/components/PerformancePanel.tsx +31 -28
  77. package/src/viewer/components/PreviewArea.tsx +1 -1
  78. package/src/viewer/components/PreviewAside.tsx +168 -0
  79. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  80. package/src/viewer/components/PropsEditor.tsx +70 -156
  81. package/src/viewer/components/ResizablePanel.tsx +103 -263
  82. package/src/viewer/components/RightSidebar.tsx +3 -9
  83. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  84. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  85. package/src/viewer/components/TopToolbar.tsx +159 -0
  86. package/src/viewer/components/VariantMatrix.tsx +42 -86
  87. package/src/viewer/components/VariantTabs.tsx +3 -3
  88. package/src/viewer/components/ViewerHeader.tsx +69 -0
  89. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  90. package/src/viewer/components/viewer-utils.ts +16 -0
  91. package/src/viewer/entry.tsx +5 -0
  92. package/src/viewer/hooks/useAppState.ts +27 -4
  93. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  94. package/src/viewer/preview-frame.html +6 -12
  95. package/src/viewer/server.ts +169 -2
  96. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  97. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  98. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  99. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  100. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  101. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  102. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  105. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  108. package/src/viewer/vendor/shared/src/index.ts +8 -0
  109. package/src/viewer/vendor/shared/src/types.ts +12 -0
  110. package/src/viewer/vite-plugin.ts +109 -4
  111. package/dist/chunk-2JIKCJX3.js.map +0 -1
  112. package/dist/chunk-CJEGT3WD.js.map +0 -1
  113. package/dist/chunk-GOVI6COW.js.map +0 -1
  114. package/dist/generate-35OIMW4Y.js +0 -252
  115. package/dist/generate-35OIMW4Y.js.map +0 -1
  116. package/dist/init-KSAAS7X3.js.map +0 -1
  117. package/dist/viewer-SBTJDMP7.js.map +0 -1
  118. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  119. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  120. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  121. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  122. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  123. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -10,9 +10,11 @@
10
10
 
11
11
  import { useMemo, useState } from "react";
12
12
  import type { FragmentDefinition, ComponentRelation, RelationshipType } from "../../core/index.js";
13
- import { ChevronRightIcon, EmptyIcon, WandIcon } from "./Icons.js";
13
+ import { ChevronRightIcon, WandIcon } from "./Icons.js";
14
14
  import { detectAllRelationships, mergeRelationships } from "../utils/detectRelationships.js";
15
- import { Stack, Text, Badge, Button, EmptyState, CodeBlock, Grid, Separator } from "@fragments-sdk/ui";
15
+ import { Stack, Text, Badge, Button, CodeBlock, Separator, Box } from "@fragments-sdk/ui";
16
+ import { TreeStructure } from "@phosphor-icons/react";
17
+ import { PanelShell } from "./PanelShell.js";
16
18
 
17
19
  interface ComponentGraphProps {
18
20
  /** Current fragment definition */
@@ -148,46 +150,24 @@ export function ComponentGraph({ fragment, allFragments, onNavigate }: Component
148
150
  }
149
151
  };
150
152
 
151
- // No fragment selected
152
- if (!fragment) {
153
- return (
154
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
155
- <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
156
- <Text weight="medium">Component Graph</Text>
157
- </div>
158
- <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
159
- <EmptyState>
160
- <EmptyState.Icon>
161
- <EmptyIcon style={{ width: '24px', height: '24px' }} />
162
- </EmptyState.Icon>
163
- <EmptyState.Description>Select a component to view its relationships</EmptyState.Description>
164
- </EmptyState>
165
- </div>
166
- </div>
167
- );
168
- }
169
-
170
153
  const hasRelations = (groupedRelations && Object.values(groupedRelations).some(g => g.length > 0)) || reverseRelations.length > 0;
171
154
  const detectedCount = detectedRelationships.length;
172
155
 
173
- // No relations defined
174
- if (!hasRelations) {
175
- return (
176
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
177
- <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
178
- <Text weight="medium">Component Graph</Text>
179
- </div>
180
- <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
181
- <EmptyState>
182
- <EmptyState.Icon>
183
- <EmptyIcon style={{ width: '24px', height: '24px' }} />
184
- </EmptyState.Icon>
185
- <EmptyState.Title>No relationships defined</EmptyState.Title>
186
- <EmptyState.Description>
187
- This component doesn't have any defined relationships. Add a <code style={{ padding: '2px 4px', background: 'var(--bg-secondary)', borderRadius: '4px', fontSize: '12px' }}>relations</code> array to your fragment definition to visualize component connections.
188
- </EmptyState.Description>
189
- <div style={{ marginTop: '16px', width: '100%' }}>
190
- <CodeBlock language="typescript">{`relations: [
156
+ // Determine empty config
157
+ const emptyConfig = !fragment ? {
158
+ icon: <TreeStructure size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
159
+ title: "No component selected",
160
+ description: "Select a component to view its relationships",
161
+ } : !hasRelations ? {
162
+ icon: <TreeStructure size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
163
+ title: "No relationships defined",
164
+ description: (
165
+ <>
166
+ Add a <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>relations</Box> array to your fragment definition to visualize component connections.
167
+ </>
168
+ ),
169
+ action: (
170
+ <CodeBlock language="typescript" code={`relations: [
191
171
  {
192
172
  component: "ButtonGroup",
193
173
  relationship: "parent",
@@ -198,116 +178,105 @@ export function ComponentGraph({ fragment, allFragments, onNavigate }: Component
198
178
  relationship: "alternative",
199
179
  note: "Use when only an icon is needed"
200
180
  }
201
- ]`}</CodeBlock>
202
- </div>
203
- </EmptyState>
204
- </div>
205
- </div>
206
- );
207
- }
181
+ ]`} />
182
+ ),
183
+ } : undefined;
184
+
185
+ // Toolbar with badges + auto-detect toggle (only when we have data)
186
+ const toolbar = fragment && hasRelations ? (
187
+ <Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
188
+ <Stack direction="row" align="center" gap="sm">
189
+ <Badge size="sm">{fragment.meta.name}</Badge>
190
+ <Badge size="sm" variant="default">
191
+ {allRelationships.length + reverseRelations.length} connection{(allRelationships.length + reverseRelations.length) !== 1 ? 's' : ''}
192
+ </Badge>
193
+ </Stack>
194
+ {detectedCount > 0 && (
195
+ <AutoDetectToggle
196
+ showAutoDetected={showAutoDetected}
197
+ onToggle={() => setShowAutoDetected(!showAutoDetected)}
198
+ />
199
+ )}
200
+ </Stack>
201
+ ) : undefined;
208
202
 
209
203
  return (
210
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
211
- {/* Header */}
212
- <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
213
- <Stack direction="row" align="center" justify="between">
214
- <Text weight="medium">Component Graph</Text>
215
- {detectedCount > 0 && (
216
- <AutoDetectToggle
217
- showAutoDetected={showAutoDetected}
218
- onToggle={() => setShowAutoDetected(!showAutoDetected)}
219
- />
204
+ <PanelShell toolbar={toolbar} empty={emptyConfig}>
205
+ {/* Defined relations */}
206
+ {groupedRelations && (
207
+ <Stack direction="column" gap="md">
208
+ {(Object.entries(groupedRelations) as [RelationshipType, ComponentRelation[]][]).map(
209
+ ([type, relations]) => {
210
+ if (relations.length === 0) return null;
211
+ const config = RELATIONSHIP_CONFIG[type];
212
+
213
+ return (
214
+ <div key={type}>
215
+ <Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
216
+ <Text size="xs" weight="medium" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', color: config.color }}>
217
+ {config.label}
218
+ </Text>
219
+ <Separator style={{ flex: 1 }} />
220
+ </Stack>
221
+ <Stack direction="column" gap="sm">
222
+ {relations.map((relation, index) => (
223
+ <RelationCard
224
+ key={`${relation.component}-${index}`}
225
+ relation={relation}
226
+ config={config}
227
+ exists={componentExists(relation.component)}
228
+ onNavigate={handleNavigate}
229
+ />
230
+ ))}
231
+ </Stack>
232
+ </div>
233
+ );
234
+ }
220
235
  )}
221
236
  </Stack>
222
- <Text size="xs" color="secondary" style={{ marginTop: '4px' }}>
223
- Relationships for <Text as="span" weight="medium" size="xs">{fragment.meta.name}</Text>
224
- {showAutoDetected && detectedCount > 0 && (
225
- <span style={{ marginLeft: '4px' }}>
226
- ({detectedCount} from variants)
227
- </span>
228
- )}
229
- </Text>
230
- </div>
231
-
232
- {/* Graph visualization */}
233
- <div style={{ flex: 1, overflowY: 'auto', padding: '16px' }}>
234
- {/* Defined relations */}
235
- {groupedRelations && (
236
- <Stack direction="column" gap="md">
237
- {(Object.entries(groupedRelations) as [RelationshipType, ComponentRelation[]][]).map(
238
- ([type, relations]) => {
239
- if (relations.length === 0) return null;
240
- const config = RELATIONSHIP_CONFIG[type];
237
+ )}
241
238
 
242
- return (
243
- <div key={type}>
244
- <Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
245
- <Text size="xs" weight="medium" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', color: config.color }}>
246
- {config.label}
247
- </Text>
248
- <div style={{ flex: 1, height: '1px', background: 'var(--border)' }} />
249
- </Stack>
250
- <Stack direction="column" gap="sm">
251
- {relations.map((relation, index) => (
252
- <RelationCard
253
- key={`${relation.component}-${index}`}
254
- relation={relation}
255
- config={config}
256
- exists={componentExists(relation.component)}
257
- onNavigate={handleNavigate}
258
- />
259
- ))}
260
- </Stack>
261
- </div>
262
- );
263
- }
264
- )}
239
+ {/* Reverse relations (inbound references) */}
240
+ {reverseRelations.length > 0 && (
241
+ <Box style={{ marginTop: '24px' }}>
242
+ <Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
243
+ <Text size="xs" weight="medium" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
244
+ Referenced By
245
+ </Text>
246
+ <Separator style={{ flex: 1 }} />
265
247
  </Stack>
266
- )}
267
-
268
- {/* Reverse relations (inbound references) */}
269
- {reverseRelations.length > 0 && (
270
- <div style={{ marginTop: '24px' }}>
271
- <Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
272
- <Text size="xs" weight="medium" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
273
- Referenced By
274
- </Text>
275
- <div style={{ flex: 1, height: '1px', background: 'var(--border)' }} />
276
- </Stack>
277
- <Stack direction="column" gap="sm">
278
- {reverseRelations.map(({ relation }, index) => {
279
- const config = RELATIONSHIP_CONFIG[relation.relationship];
280
- return (
281
- <RelationCard
282
- key={`reverse-${relation.component}-${index}`}
283
- relation={relation}
284
- config={config}
285
- exists={componentExists(relation.component)}
286
- onNavigate={handleNavigate}
287
- isReverse
288
- />
289
- );
290
- })}
291
- </Stack>
292
- </div>
293
- )}
248
+ <Stack direction="column" gap="sm">
249
+ {reverseRelations.map(({ relation }, index) => {
250
+ const config = RELATIONSHIP_CONFIG[relation.relationship];
251
+ return (
252
+ <RelationCard
253
+ key={`reverse-${relation.component}-${index}`}
254
+ relation={relation}
255
+ config={config}
256
+ exists={componentExists(relation.component)}
257
+ onNavigate={handleNavigate}
258
+ isReverse
259
+ />
260
+ );
261
+ })}
262
+ </Stack>
263
+ </Box>
264
+ )}
294
265
 
295
- {/* Legend */}
296
- <div style={{ marginTop: '32px', paddingTop: '16px', borderTop: '1px solid var(--border)' }}>
297
- <Text size="xs" weight="medium" color="secondary" style={{ marginBottom: '8px' }}>Legend</Text>
298
- <Grid columns={2} gap="sm">
299
- {(Object.entries(RELATIONSHIP_CONFIG) as [RelationshipType, typeof RELATIONSHIP_CONFIG[RelationshipType]][]).map(
300
- ([type, config]) => (
301
- <Stack key={type} direction="row" align="center" gap="sm">
302
- <span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: config.bgColor, border: `1px solid ${config.color}` }} />
303
- <Text size="xs" color="secondary">{config.label}</Text>
304
- </Stack>
305
- )
306
- )}
307
- </Grid>
308
- </div>
309
- </div>
310
- </div>
266
+ {/* Legend */}
267
+ <Box borderTop style={{ marginTop: '24px', paddingTop: '12px' }}>
268
+ <Stack direction="row" gap="md" style={{ flexWrap: 'wrap' }}>
269
+ {(Object.entries(RELATIONSHIP_CONFIG) as [RelationshipType, typeof RELATIONSHIP_CONFIG[RelationshipType]][]).map(
270
+ ([type, config]) => (
271
+ <Stack key={type} direction="row" align="center" gap="xs">
272
+ <span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: config.color, flexShrink: 0 }} />
273
+ <Text size="xs" color="tertiary">{config.label}</Text>
274
+ </Stack>
275
+ )
276
+ )}
277
+ </Stack>
278
+ </Box>
279
+ </PanelShell>
311
280
  );
312
281
  }
313
282
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { memo } from 'react';
7
7
  import type { FragmentContract } from '../../core/index.js';
8
- import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge } from '@fragments-sdk/ui';
8
+ import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge, Box } from '@fragments-sdk/ui';
9
9
 
10
10
  interface ContractPanelProps {
11
11
  contract?: FragmentContract;
@@ -37,7 +37,7 @@ export const ContractPanel = memo(function ContractPanel({
37
37
  </EmptyState.Icon>
38
38
  <EmptyState.Title>No contract metadata</EmptyState.Title>
39
39
  <EmptyState.Description>
40
- Add a <code style={{ background: 'var(--bg-hover)', padding: '2px 4px', borderRadius: '4px' }}>contract</code> field to the fragment definition to enable AI agent features.
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
41
  </EmptyState.Description>
42
42
  <EmptyState.Actions>
43
43
  <CodeBlock
@@ -55,7 +55,7 @@ export const ContractPanel = memo(function ContractPanel({
55
55
  }
56
56
 
57
57
  return (
58
- <div style={{ padding: '16px', fontSize: '14px' }}>
58
+ <Box padding="md" style={{ fontSize: '14px' }}>
59
59
  <Stack gap="lg">
60
60
  {/* Props Summary */}
61
61
  {contract.propsSummary && contract.propsSummary.length > 0 && (
@@ -110,12 +110,11 @@ export const ContractPanel = memo(function ContractPanel({
110
110
  <Section title="Accessibility Rules">
111
111
  <Stack gap="sm">
112
112
  {contract.a11yRules.map((rule, i) => (
113
+ <Box key={i} padding="sm" background="secondary" rounded="sm">
113
114
  <Stack
114
- key={i}
115
115
  direction="row"
116
116
  align="center"
117
117
  gap="sm"
118
- style={{ padding: '8px', background: 'var(--bg-hover)', borderRadius: '4px' }}
119
118
  >
120
119
  <span style={{ color: '#22c55e' }}>
121
120
  <svg style={{ width: '16px', height: '16px' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -124,6 +123,7 @@ export const ContractPanel = memo(function ContractPanel({
124
123
  </span>
125
124
  <Text font="mono" size="xs" color="secondary">{rule}</Text>
126
125
  </Stack>
126
+ </Box>
127
127
  ))}
128
128
  </Stack>
129
129
  </Section>
@@ -160,7 +160,7 @@ export const ContractPanel = memo(function ContractPanel({
160
160
  </Alert>
161
161
  )}
162
162
  </Stack>
163
- </div>
163
+ </Box>
164
164
  );
165
165
  });
166
166
 
@@ -0,0 +1,54 @@
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,5 +1,6 @@
1
1
  import { useRef, useEffect, useState, useMemo, useCallback } from "react";
2
2
  import { FigmaIcon } from "./Icons.js";
3
+ import { Stack, Text, Button } from '@fragments-sdk/ui';
3
4
 
4
5
  interface FigmaEmbedProps {
5
6
  /** Current Figma URL to display */
@@ -124,18 +125,19 @@ export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, styl
124
125
  // If we can't parse the URL, show error
125
126
  if (!currentParsed) {
126
127
  return (
127
- <div className={className} style={{ ...style, display: "flex", alignItems: "center", justifyContent: "center" }}>
128
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', color: 'var(--text-tertiary)', padding: '16px', textAlign: 'center' }}>
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' }}>
129
130
  <FigmaIcon style={{ width: '24px', height: '24px' }} />
130
- <span style={{ fontSize: '12px' }}>Unable to embed Figma design</span>
131
- <button
131
+ <Text size="xs">Unable to embed Figma design</Text>
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
132
135
  onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
133
- style={{ fontSize: '12px', color: 'var(--color-accent)', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}
134
136
  >
135
137
  Open in Figma
136
- </button>
137
- </div>
138
- </div>
138
+ </Button>
139
+ </Stack>
140
+ </Stack>
139
141
  );
140
142
  }
141
143
 
@@ -143,22 +145,22 @@ export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, styl
143
145
  <div className={className} style={{ ...style, position: "relative", overflow: "hidden" }}>
144
146
  {/* Loading overlay - shows while current iframe is loading */}
145
147
  {!isCurrentLoaded && (
146
- <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
147
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
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">
148
150
  <FigmaIcon style={{ width: '20px', height: '20px', color: 'var(--text-tertiary)' }} />
149
- <span style={{ fontSize: '12px', color: 'var(--text-tertiary)' }}>Loading Figma...</span>
150
- </div>
151
- </div>
151
+ <Text size="xs" color="tertiary">Loading Figma...</Text>
152
+ </Stack>
153
+ </Stack>
152
154
  )}
153
155
 
154
156
  {/* Error overlay */}
155
157
  {error && (
156
- <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
157
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', color: 'var(--text-tertiary)' }}>
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)' }}>
158
160
  <FigmaIcon style={{ width: '24px', height: '24px' }} />
159
- <span style={{ fontSize: '12px' }}>{error}</span>
160
- </div>
161
- </div>
161
+ <Text size="xs">{error}</Text>
162
+ </Stack>
163
+ </Stack>
162
164
  )}
163
165
 
164
166
  {/*