@fragments-sdk/cli 0.8.1 → 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 (128) hide show
  1. package/dist/bin.js +517 -77
  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-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
  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-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +2 -2
  36. package/src/bin.ts +26 -0
  37. package/src/build.ts +12 -2
  38. package/src/commands/build.ts +16 -2
  39. package/src/commands/doctor.ts +498 -0
  40. package/src/commands/generate.ts +383 -68
  41. package/src/commands/init-framework.ts +1 -1
  42. package/src/commands/init.ts +9 -51
  43. package/src/core/config.ts +15 -2
  44. package/src/core/generators/typescript-extractor.ts +10 -0
  45. package/src/core/index.ts +15 -0
  46. package/src/core/schema.ts +10 -2
  47. package/src/core/storyFilters.test.ts +350 -0
  48. package/src/core/storyFilters.ts +253 -0
  49. package/src/core/types.ts +22 -0
  50. package/src/migrate/converter.ts +9 -1
  51. package/src/migrate/parser.ts +2 -0
  52. package/src/migrate/types.ts +2 -0
  53. package/src/setup.ts +69 -24
  54. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  55. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  56. package/src/viewer/components/ActionsPanel.tsx +31 -29
  57. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  58. package/src/viewer/components/App.tsx +187 -740
  59. package/src/viewer/components/BottomPanel.tsx +228 -132
  60. package/src/viewer/components/CodePanel.tsx +1 -1
  61. package/src/viewer/components/CommandPalette.tsx +7 -10
  62. package/src/viewer/components/ComponentDocView.tsx +164 -0
  63. package/src/viewer/components/ComponentGraph.tsx +111 -142
  64. package/src/viewer/components/ContractPanel.tsx +6 -6
  65. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  66. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  67. package/src/viewer/components/FragmentEditor.tsx +92 -115
  68. package/src/viewer/components/HeaderSearch.tsx +24 -0
  69. package/src/viewer/components/HealthDashboard.tsx +16 -2
  70. package/src/viewer/components/Icons.tsx +9 -0
  71. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  72. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  73. package/src/viewer/components/LandingPage.tsx +3 -3
  74. package/src/viewer/components/LeftSidebar.tsx +141 -63
  75. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  76. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  77. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  78. package/src/viewer/components/PanelShell.tsx +161 -0
  79. package/src/viewer/components/PerformancePanel.tsx +31 -28
  80. package/src/viewer/components/PreviewArea.tsx +1 -1
  81. package/src/viewer/components/PreviewAside.tsx +168 -0
  82. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  83. package/src/viewer/components/PropsEditor.tsx +70 -156
  84. package/src/viewer/components/ResizablePanel.tsx +103 -263
  85. package/src/viewer/components/RightSidebar.tsx +3 -9
  86. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  87. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  88. package/src/viewer/components/TopToolbar.tsx +159 -0
  89. package/src/viewer/components/VariantMatrix.tsx +42 -86
  90. package/src/viewer/components/VariantTabs.tsx +3 -3
  91. package/src/viewer/components/ViewerHeader.tsx +69 -0
  92. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  93. package/src/viewer/components/viewer-utils.ts +16 -0
  94. package/src/viewer/entry.tsx +5 -0
  95. package/src/viewer/hooks/useAppState.ts +27 -4
  96. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  97. package/src/viewer/preview-frame.html +6 -12
  98. package/src/viewer/server.ts +184 -6
  99. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  100. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  101. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  102. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  105. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  108. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  109. package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
  110. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
  111. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
  112. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
  113. package/src/viewer/vendor/shared/src/index.ts +8 -0
  114. package/src/viewer/vendor/shared/src/types.ts +12 -0
  115. package/src/viewer/vite-plugin.ts +109 -4
  116. package/dist/chunk-2JIKCJX3.js.map +0 -1
  117. package/dist/chunk-CJEGT3WD.js.map +0 -1
  118. package/dist/chunk-GOVI6COW.js.map +0 -1
  119. package/dist/generate-35OIMW4Y.js +0 -252
  120. package/dist/generate-35OIMW4Y.js.map +0 -1
  121. package/dist/init-KFYN37ZY.js.map +0 -1
  122. package/dist/viewer-HZK4BSDK.js.map +0 -1
  123. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  124. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  125. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  126. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  127. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  128. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -0,0 +1,102 @@
1
+ import { Stack, Text, Alert, Box } from "@fragments-sdk/ui";
2
+
3
+ interface LoadErrorMessageProps {
4
+ error: { message: string; dependencies: string[] };
5
+ componentName?: string;
6
+ }
7
+
8
+ export function LoadErrorMessage({ error, componentName }: LoadErrorMessageProps) {
9
+ const deps = error.dependencies || [];
10
+ const errorMessage = error.message || "Unknown error";
11
+
12
+ // Determine if the error is a missing module/dependency issue
13
+ const isModuleError =
14
+ /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(
15
+ errorMessage
16
+ );
17
+
18
+ // Only suggest packages if the error is actually about missing modules
19
+ let suggestedPackages: string[] = [];
20
+ if (isModuleError) {
21
+ if (deps.length > 0) {
22
+ suggestedPackages = [...deps];
23
+ } else {
24
+ const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
25
+ if (match) {
26
+ suggestedPackages = [match[1]];
27
+ }
28
+ }
29
+ }
30
+
31
+ const hasMissingDeps = suggestedPackages.length > 0;
32
+ const installCmd = `pnpm add ${suggestedPackages.join(" ")}`;
33
+
34
+ return (
35
+ <Stack align="center" justify="center" style={{ height: "100%", padding: "24px" }}>
36
+ <Alert variant="warning">
37
+ <Alert.Body>
38
+ <Alert.Title>{hasMissingDeps ? "Missing Dependencies" : "Failed to Load"}</Alert.Title>
39
+ <Alert.Content>
40
+ <Stack direction="column" gap="sm">
41
+ {hasMissingDeps ? (
42
+ <>
43
+ <Text size="xs" color="secondary">
44
+ {componentName ? `${componentName} requires` : "This component requires"}{" "}
45
+ packages that are not installed in your project.
46
+ </Text>
47
+ <Text size="xs" weight="semibold" color="secondary">
48
+ Install with:
49
+ </Text>
50
+ <Box
51
+ as="code"
52
+ padding="sm"
53
+ background="tertiary"
54
+ rounded="sm"
55
+ style={{
56
+ display: "block",
57
+ fontFamily: "monospace",
58
+ fontSize: "12px",
59
+ userSelect: "all",
60
+ }}
61
+ >
62
+ {installCmd}
63
+ </Box>
64
+ <Text size="xs" color="tertiary">
65
+ After installing, restart the dev server.
66
+ </Text>
67
+ </>
68
+ ) : (
69
+ <>
70
+ <Text size="xs" color="secondary">
71
+ {componentName ? `${componentName} couldn't` : "This component couldn't"} be
72
+ loaded. This may be due to a schema validation error or missing imports.
73
+ </Text>
74
+ <Text size="xs" weight="semibold" color="secondary">
75
+ Error:
76
+ </Text>
77
+ <Box
78
+ as="pre"
79
+ padding="sm"
80
+ background="tertiary"
81
+ rounded="sm"
82
+ style={{
83
+ fontFamily: "monospace",
84
+ fontSize: "11px",
85
+ whiteSpace: "pre-wrap",
86
+ wordBreak: "break-word",
87
+ margin: 0,
88
+ maxHeight: "200px",
89
+ overflow: "auto",
90
+ }}
91
+ >
92
+ {errorMessage}
93
+ </Box>
94
+ </>
95
+ )}
96
+ </Stack>
97
+ </Alert.Content>
98
+ </Alert.Body>
99
+ </Alert>
100
+ </Stack>
101
+ );
102
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { useState, type ReactNode } from "react";
10
+ import { Box, Stack, Text, Button, Menu } from "@fragments-sdk/ui";
10
11
  import { ErrorBoundary } from "./ErrorBoundary.js";
11
12
  import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
12
13
  import { ChevronDownIcon } from "./Icons.js";
@@ -65,9 +66,6 @@ export function MultiViewportPreview({
65
66
  useIframeIsolation = true,
66
67
  }: MultiViewportPreviewProps) {
67
68
  const [selectedMobile, setSelectedMobile] = useState<MobilePreset>(MOBILE_PRESETS[0]);
68
- const [showMobileDropdown, setShowMobileDropdown] = useState(false);
69
- const [hoveredPreset, setHoveredPreset] = useState<string | null>(null);
70
- const [mobileButtonHovered, setMobileButtonHovered] = useState(false);
71
69
 
72
70
  // Build viewports with selected mobile preset
73
71
  const viewports: ViewportConfig[] = [
@@ -77,123 +75,54 @@ export function MultiViewportPreview({
77
75
  ];
78
76
 
79
77
  return (
80
- <div style={{
81
- height: '100%',
82
- display: 'flex',
83
- flexDirection: 'column',
84
- background: 'var(--bg-primary)',
85
- }}>
78
+ <Box height="100%" display="flex" style={{ flexDirection: 'column' }} background="primary">
86
79
  {/* Header */}
87
- <div style={{
88
- flexShrink: 0,
89
- padding: '12px 16px',
90
- borderBottom: '1px solid var(--border)',
91
- background: 'var(--bg-secondary)',
92
- display: 'flex',
93
- alignItems: 'center',
94
- justifyContent: 'center',
95
- gap: 32,
96
- }}>
97
- {/* Mobile with dropdown */}
98
- <div style={{ position: 'relative' }}>
99
- <button
100
- onClick={() => setShowMobileDropdown(!showMobileDropdown)}
101
- onMouseEnter={() => setMobileButtonHovered(true)}
102
- onMouseLeave={() => setMobileButtonHovered(false)}
103
- style={{
104
- display: 'flex',
105
- alignItems: 'center',
106
- gap: 8,
107
- fontSize: 14,
108
- padding: '4px 8px',
109
- borderRadius: 4,
110
- transition: 'background-color 0.15s',
111
- background: mobileButtonHovered ? 'var(--bg-hover)' : 'transparent',
112
- border: 'none',
113
- cursor: 'pointer',
114
- color: 'inherit',
115
- }}
116
- >
117
- <span style={{ fontSize: 18 }}>📱</span>
118
- <span style={{ fontWeight: 500, color: 'var(--text-secondary)' }}>Mobile</span>
119
- <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>({selectedMobile.width}px)</span>
120
- <ChevronDownIcon style={{ width: 12, height: 12, color: 'var(--text-tertiary)' }} />
121
- </button>
122
-
123
- {showMobileDropdown && (
124
- <>
125
- <div
126
- style={{
127
- position: 'fixed',
128
- inset: 0,
129
- zIndex: 10,
130
- }}
131
- onClick={() => setShowMobileDropdown(false)}
132
- />
133
- <div style={{
134
- position: 'absolute',
135
- top: '100%',
136
- left: 0,
137
- marginTop: 4,
138
- zIndex: 20,
139
- background: 'var(--bg-elevated)',
140
- border: '1px solid var(--border)',
141
- borderRadius: 8,
142
- boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
143
- padding: '4px 0',
144
- minWidth: 180,
145
- }}>
146
- {MOBILE_PRESETS.map((preset) => (
147
- <button
148
- key={preset.name}
149
- onClick={() => {
150
- setSelectedMobile(preset);
151
- setShowMobileDropdown(false);
152
- }}
153
- onMouseEnter={() => setHoveredPreset(preset.name)}
154
- onMouseLeave={() => setHoveredPreset(null)}
155
- style={{
156
- width: '100%',
157
- padding: '6px 12px',
158
- textAlign: 'left',
159
- fontSize: 12,
160
- transition: 'background-color 0.15s',
161
- display: 'flex',
162
- alignItems: 'center',
163
- justifyContent: 'space-between',
164
- color: selectedMobile.name === preset.name ? '#3b82f6' : 'var(--text-secondary)',
165
- background: hoveredPreset === preset.name ? 'var(--bg-hover)' : 'transparent',
166
- border: 'none',
167
- cursor: 'pointer',
168
- }}
169
- >
170
- <span>{preset.name}</span>
171
- <span style={{ color: 'var(--text-tertiary)' }}>{preset.width}×{preset.height}</span>
172
- </button>
173
- ))}
174
- </div>
175
- </>
176
- )}
177
- </div>
80
+ <Box paddingX="md" paddingY="sm" borderBottom background="secondary" style={{ flexShrink: 0 }}>
81
+ <Stack direction="row" align="center" justify="center" gap="lg">
82
+ {/* Mobile with dropdown */}
83
+ <Menu>
84
+ <Menu.Trigger>
85
+ <Button variant="ghost" size="sm">
86
+ <span style={{ fontSize: 18 }}>📱</span>
87
+ <Text size="sm" weight="medium" color="secondary">Mobile</Text>
88
+ <Text size="xs" color="tertiary">({selectedMobile.width}px)</Text>
89
+ <ChevronDownIcon style={{ width: 12, height: 12 }} />
90
+ </Button>
91
+ </Menu.Trigger>
92
+ <Menu.Content>
93
+ {MOBILE_PRESETS.map((preset) => (
94
+ <Menu.Item
95
+ key={preset.name}
96
+ onClick={() => setSelectedMobile(preset)}
97
+ >
98
+ <Stack direction="row" justify="between" style={{ width: '100%' }}>
99
+ <Text size="xs">{preset.name}</Text>
100
+ <Text size="xs" color="tertiary">{preset.width}×{preset.height}</Text>
101
+ </Stack>
102
+ </Menu.Item>
103
+ ))}
104
+ </Menu.Content>
105
+ </Menu>
178
106
 
179
- {/* Tablet */}
180
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14 }}>
181
- <span style={{ fontSize: 18 }}>📱</span>
182
- <span style={{ fontWeight: 500, color: 'var(--text-secondary)' }}>Tablet</span>
183
- <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>(768px)</span>
184
- </div>
107
+ {/* Tablet */}
108
+ <Stack direction="row" align="center" gap="sm">
109
+ <span style={{ fontSize: 18 }}>📱</span>
110
+ <Text size="sm" weight="medium" color="secondary">Tablet</Text>
111
+ <Text size="xs" color="tertiary">(768px)</Text>
112
+ </Stack>
185
113
 
186
- {/* Desktop */}
187
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14 }}>
188
- <span style={{ fontSize: 18 }}>🖥️</span>
189
- <span style={{ fontWeight: 500, color: 'var(--text-secondary)' }}>Desktop</span>
190
- <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>(1280px)</span>
191
- </div>
192
- </div>
114
+ {/* Desktop */}
115
+ <Stack direction="row" align="center" gap="sm">
116
+ <span style={{ fontSize: 18 }}>🖥️</span>
117
+ <Text size="sm" weight="medium" color="secondary">Desktop</Text>
118
+ <Text size="xs" color="tertiary">(1280px)</Text>
119
+ </Stack>
120
+ </Stack>
121
+ </Box>
193
122
 
194
123
  {/* Viewport panels with horizontal scroll */}
195
- <div style={{ flex: 1, overflowX: 'auto', overflowY: 'auto' }}>
196
- <div style={{ display: 'flex', gap: 32, padding: 32, minWidth: 'max-content' }}>
124
+ <Box overflow="auto" style={{ flex: 1 }}>
125
+ <Stack direction="row" gap="lg" style={{ padding: 32, minWidth: 'max-content' }}>
197
126
  {viewports.map((vp) => (
198
127
  <ViewportPanel
199
128
  key={`${vp.name}-${vp.width}`}
@@ -206,9 +135,9 @@ export function MultiViewportPreview({
206
135
  useIframeIsolation={useIframeIsolation}
207
136
  />
208
137
  ))}
209
- </div>
210
- </div>
211
- </div>
138
+ </Stack>
139
+ </Box>
140
+ </Box>
212
141
  );
213
142
  }
214
143
 
@@ -293,16 +222,11 @@ function DeviceMockup({
293
222
  const screenHeight = height;
294
223
 
295
224
  return (
296
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
225
+ <Stack align="center">
297
226
  {/* Label */}
298
- <div style={{
299
- marginBottom: 12,
300
- fontSize: 14,
301
- fontWeight: 500,
302
- color: 'var(--text-tertiary)',
303
- }}>
227
+ <Text size="sm" weight="medium" color="tertiary" style={{ marginBottom: 12 }}>
304
228
  {label}
305
- </div>
229
+ </Text>
306
230
 
307
231
  {/* Device frame */}
308
232
  <div
@@ -432,9 +356,9 @@ function DeviceMockup({
432
356
  <ErrorBoundary
433
357
  componentName={componentName}
434
358
  fallback={
435
- <div style={{ fontSize: 12, color: '#ef4444', padding: 8 }}>
359
+ <Text size="xs" color="tertiary" style={{ padding: 8 }}>
436
360
  Error rendering at {width}px
437
- </div>
361
+ </Text>
438
362
  }
439
363
  >
440
364
  {renderContent()}
@@ -458,7 +382,7 @@ function DeviceMockup({
458
382
  </div>
459
383
  </div>
460
384
  </div>
461
- </div>
385
+ </Stack>
462
386
  );
463
387
  }
464
388
 
@@ -486,19 +410,14 @@ function DesktopMockup({
486
410
  useIframeIsolation,
487
411
  }: DesktopMockupProps) {
488
412
  return (
489
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
413
+ <Stack align="center">
490
414
  {/* Label */}
491
- <div style={{
492
- marginBottom: 12,
493
- fontSize: 14,
494
- fontWeight: 500,
495
- color: 'var(--text-tertiary)',
496
- }}>
415
+ <Text size="sm" weight="medium" color="tertiary" style={{ marginBottom: 12 }}>
497
416
  {label}
498
- </div>
417
+ </Text>
499
418
 
500
419
  {/* Monitor frame */}
501
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
420
+ <Stack align="center">
502
421
  {/* Screen */}
503
422
  <div
504
423
  style={{
@@ -569,9 +488,9 @@ function DesktopMockup({
569
488
  <ErrorBoundary
570
489
  componentName={componentName}
571
490
  fallback={
572
- <div style={{ fontSize: 12, color: '#ef4444', padding: 8 }}>
491
+ <Text size="xs" color="tertiary" style={{ padding: 8 }}>
573
492
  Error rendering at {width}px
574
- </div>
493
+ </Text>
575
494
  }
576
495
  >
577
496
  {renderContent()}
@@ -595,8 +514,8 @@ function DesktopMockup({
595
514
  borderRadius: 9999,
596
515
  boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
597
516
  }} />
598
- </div>
599
- </div>
517
+ </Stack>
518
+ </Stack>
600
519
  );
601
520
  }
602
521
 
@@ -0,0 +1,59 @@
1
+ import type { FragmentDefinition } from "../../core/index.js";
2
+ import { Stack, Text, Alert, EmptyState } from "@fragments-sdk/ui";
3
+ import { LoadErrorMessage } from "./LoadErrorMessage.js";
4
+
5
+ interface NoVariantsMessageProps {
6
+ fragment?: FragmentDefinition;
7
+ }
8
+
9
+ export function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
10
+ // Check for load error (missing dependencies, schema errors, etc.)
11
+ const loadError = (fragment as any)?._loadError;
12
+ if (loadError) {
13
+ return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
14
+ }
15
+
16
+ const skippedVariants = (fragment?._generated as any)?.skippedVariants;
17
+
18
+ if (!skippedVariants || skippedVariants.length === 0) {
19
+ return (
20
+ <EmptyState style={{ height: "100%" }}>
21
+ <EmptyState.Description>No variants defined</EmptyState.Description>
22
+ </EmptyState>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <Stack align="center" justify="center" style={{ height: "100%", padding: "24px" }}>
28
+ <Alert variant="info">
29
+ <Alert.Body>
30
+ <Alert.Title>
31
+ {skippedVariants.length} variant{skippedVariants.length === 1 ? "" : "s"} skipped
32
+ </Alert.Title>
33
+ <Alert.Content>
34
+ <Stack direction="column" gap="sm">
35
+ <Text size="xs" color="secondary">
36
+ These variants couldn't be rendered because they use syntax the parser doesn't
37
+ support yet:
38
+ </Text>
39
+ <ul style={{ marginTop: "4px", marginLeft: "16px", listStyleType: "disc" }}>
40
+ {skippedVariants.map((sv: any, i: number) => (
41
+ <li key={i}>
42
+ <Text size="xs" color="secondary">
43
+ <Text as="span" size="xs" weight="semibold">
44
+ {sv.name}:
45
+ </Text>{" "}
46
+ <Text as="span" size="xs" color="tertiary">
47
+ {sv.reason}
48
+ </Text>
49
+ </Text>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </Stack>
54
+ </Alert.Content>
55
+ </Alert.Body>
56
+ </Alert>
57
+ </Stack>
58
+ );
59
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * PanelShell — Consistent wrapper for bottom panel tabs.
3
+ *
4
+ * Provides standardized structure for all 5 bottom panel tabs:
5
+ * - Optional toolbar (badges, filters, action buttons)
6
+ * - Scrollable body with consistent padding
7
+ * - Loading skeleton state
8
+ * - Empty state with icon, title, description, action
9
+ * - Error state with Alert
10
+ *
11
+ * The tab label (e.g., "Graph", "Performance") is rendered by
12
+ * ResizablePanel — PanelShell does NOT render a title.
13
+ */
14
+
15
+ import type { ReactNode } from "react";
16
+ import { Stack, Box, EmptyState, Alert } from "@fragments-sdk/ui";
17
+
18
+ interface PanelShellEmptyConfig {
19
+ /** Phosphor icon element */
20
+ icon: ReactNode;
21
+ /** Empty state title */
22
+ title: string;
23
+ /** Optional description */
24
+ description?: ReactNode;
25
+ /** Optional action slot (e.g. Button, CodeBlock) */
26
+ action?: ReactNode;
27
+ }
28
+
29
+ export interface PanelShellProps {
30
+ /** Optional toolbar content (badges, filters, action buttons) */
31
+ toolbar?: ReactNode;
32
+ /** Main body content */
33
+ children: ReactNode;
34
+ /** Show loading skeleton instead of children */
35
+ loading?: boolean;
36
+ /** Custom loading content (defaults to generic skeleton) */
37
+ loadingContent?: ReactNode;
38
+ /** Empty state config — renders when provided (instead of children) */
39
+ empty?: PanelShellEmptyConfig;
40
+ /** Error message — renders Alert */
41
+ error?: string;
42
+ /** Body padding (default: "md") */
43
+ bodyPadding?: "sm" | "md" | "lg" | "none";
44
+ }
45
+
46
+ function DefaultSkeleton() {
47
+ return (
48
+ <Stack gap="md">
49
+ {[0, 1, 2].map((i) => (
50
+ <div
51
+ key={i}
52
+ style={{
53
+ height: "48px",
54
+ borderRadius: "8px",
55
+ background: "var(--bg-hover)",
56
+ animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
57
+ animationDelay: `${i * 100}ms`,
58
+ }}
59
+ />
60
+ ))}
61
+ </Stack>
62
+ );
63
+ }
64
+
65
+ export function PanelShell({
66
+ toolbar,
67
+ children,
68
+ loading = false,
69
+ loadingContent,
70
+ empty,
71
+ error,
72
+ bodyPadding = "md",
73
+ }: PanelShellProps) {
74
+ const padding = bodyPadding !== "none" ? bodyPadding : undefined;
75
+
76
+ const renderBody = (): ReactNode => {
77
+ if (loading) {
78
+ return (
79
+ <Box overflow="auto" padding={padding} style={{ flex: 1 }}>
80
+ {loadingContent || <DefaultSkeleton />}
81
+ </Box>
82
+ );
83
+ }
84
+
85
+ if (error) {
86
+ return (
87
+ <Box overflow="auto" padding={padding} style={{ flex: 1 }}>
88
+ <Alert variant="danger">{error}</Alert>
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ if (empty) {
94
+ return (
95
+ <Stack
96
+ align="center"
97
+ justify="center"
98
+ style={{ flex: 1, padding: "32px" }}
99
+ >
100
+ <EmptyState>
101
+ <EmptyState.Icon>
102
+ <Box
103
+ rounded="full"
104
+ display="flex"
105
+ style={{
106
+ width: 48,
107
+ height: 48,
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ background: "var(--bg-hover)",
111
+ }}
112
+ >
113
+ {empty.icon}
114
+ </Box>
115
+ </EmptyState.Icon>
116
+ <EmptyState.Title>{empty.title}</EmptyState.Title>
117
+ {empty.description && (
118
+ <EmptyState.Description>
119
+ {empty.description}
120
+ </EmptyState.Description>
121
+ )}
122
+ {empty.action && (
123
+ <Box
124
+ style={{ marginTop: "16px", width: "100%", maxWidth: "400px" }}
125
+ >
126
+ {empty.action}
127
+ </Box>
128
+ )}
129
+ </EmptyState>
130
+ </Stack>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <Box overflow="auto" padding={padding} style={{ flex: 1 }}>
136
+ {children}
137
+ </Box>
138
+ );
139
+ };
140
+
141
+ return (
142
+ <Stack style={{ height: "100%" }}>
143
+ {toolbar && (
144
+ <Box
145
+ paddingX="sm"
146
+ paddingY="xs"
147
+ borderBottom
148
+ style={{
149
+ flexShrink: 0,
150
+ minHeight: "36px",
151
+ display: "flex",
152
+ alignItems: "center",
153
+ }}
154
+ >
155
+ {toolbar}
156
+ </Box>
157
+ )}
158
+ {renderBody()}
159
+ </Stack>
160
+ );
161
+ }