@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.
- package/dist/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /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
|
-
<
|
|
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
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
<
|
|
196
|
-
<
|
|
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
|
-
</
|
|
210
|
-
</
|
|
211
|
-
</
|
|
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
|
-
<
|
|
225
|
+
<Stack align="center">
|
|
297
226
|
{/* Label */}
|
|
298
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
359
|
+
<Text size="xs" color="tertiary" style={{ padding: 8 }}>
|
|
436
360
|
Error rendering at {width}px
|
|
437
|
-
</
|
|
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
|
-
</
|
|
385
|
+
</Stack>
|
|
462
386
|
);
|
|
463
387
|
}
|
|
464
388
|
|
|
@@ -486,19 +410,14 @@ function DesktopMockup({
|
|
|
486
410
|
useIframeIsolation,
|
|
487
411
|
}: DesktopMockupProps) {
|
|
488
412
|
return (
|
|
489
|
-
<
|
|
413
|
+
<Stack align="center">
|
|
490
414
|
{/* Label */}
|
|
491
|
-
<
|
|
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
|
-
</
|
|
417
|
+
</Text>
|
|
499
418
|
|
|
500
419
|
{/* Monitor frame */}
|
|
501
|
-
<
|
|
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
|
-
<
|
|
491
|
+
<Text size="xs" color="tertiary" style={{ padding: 8 }}>
|
|
573
492
|
Error rendering at {width}px
|
|
574
|
-
</
|
|
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
|
-
</
|
|
599
|
-
</
|
|
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
|
+
}
|