@fragments-sdk/cli 0.7.9 → 0.7.11
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 +13 -13
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CWKQQR6C.js → chunk-57OW43NL.js} +3 -3
- package/dist/chunk-57OW43NL.js.map +1 -0
- package/dist/{chunk-AA6CAHCZ.js → chunk-7CRC46HV.js} +2 -2
- package/dist/chunk-7CRC46HV.js.map +1 -0
- package/dist/{chunk-3JPJTU25.js → chunk-CRTN6BIW.js} +5 -5
- package/dist/chunk-CRTN6BIW.js.map +1 -0
- package/dist/{chunk-LHIIBI6F.js → chunk-M42XIHPV.js} +2 -2
- package/dist/{chunk-2EFVPE5Q.js → chunk-TQOGBAOZ.js} +2 -2
- package/dist/chunk-TQOGBAOZ.js.map +1 -0
- package/dist/core/index.d.ts +1944 -0
- package/dist/{core-YAPWXDZW.js → core/index.js} +5 -5
- package/dist/defineFragment-C6PFzZyo.d.ts +656 -0
- package/dist/{generate-LEBVZCCH.js → generate-ZPERYZLF.js} +4 -4
- package/dist/index.d.ts +4 -159
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-4VXL3Q6N.js → init-GID2DXB3.js} +69 -7
- package/dist/init-GID2DXB3.js.map +1 -0
- package/dist/mcp-bin.js +3 -3
- package/dist/{scan-3NYSRF6G.js → scan-BSMLGBX4.js} +5 -5
- package/dist/{service-HL6TMP3B.js → service-QACVPR37.js} +3 -3
- package/dist/{static-viewer-KLD24I4R.js → static-viewer-2RQD5QLR.js} +3 -3
- package/dist/{test-Y7YZOJLE.js → test-36UELXTE.js} +3 -3
- package/dist/{tokens-M4FCJKBK.js → tokens-A3BZIQPB.js} +4 -4
- package/dist/{viewer-ZWQQ74FV.js → viewer-CNLZQUFO.js} +156 -32
- package/dist/viewer-CNLZQUFO.js.map +1 -0
- package/package.json +8 -2
- package/src/commands/add.ts +1 -1
- package/src/commands/init.ts +84 -4
- package/src/core/defineFragment.ts +1 -1
- package/src/core/figma.ts +1 -1
- package/src/core/index.ts +2 -2
- package/src/core/loader.ts +3 -3
- package/src/core/schema.ts +1 -1
- package/src/index.ts +6 -0
- package/src/migrate/converter.ts +1 -1
- package/src/service/snippet-validation.test.ts +5 -5
- package/src/service/snippet-validation.ts +0 -1
- package/src/viewer/__tests__/viewer-integration.test.ts +16 -23
- package/src/viewer/components/AccessibilityPanel.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +1 -1
- package/src/viewer/components/App.tsx +563 -166
- package/src/viewer/components/BottomPanel.tsx +1 -1
- package/src/viewer/components/CodePanel.naming.test.tsx +1 -2
- package/src/viewer/components/CodePanel.tsx +1 -2
- package/src/viewer/components/CommandPalette.tsx +1 -1
- package/src/viewer/components/ComponentGraph.tsx +1 -1
- package/src/viewer/components/ComponentHeader.tsx +1 -1
- package/src/viewer/components/ContractPanel.tsx +1 -1
- package/src/viewer/components/ErrorBoundary.tsx +1 -1
- package/src/viewer/components/HealthDashboard.tsx +1 -1
- package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
- package/src/viewer/components/InteractionsPanel.tsx +1 -1
- package/src/viewer/components/IsolatedRender.tsx +1 -1
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
- package/src/viewer/components/LandingPage.tsx +1 -1
- package/src/viewer/components/Layout.tsx +16 -13
- package/src/viewer/components/LeftSidebar.tsx +105 -18
- package/src/viewer/components/MultiViewportPreview.tsx +1 -1
- package/src/viewer/components/PreviewArea.tsx +22 -13
- package/src/viewer/components/PreviewFrameHost.tsx +0 -4
- package/src/viewer/components/PreviewToolbar.tsx +1 -1
- package/src/viewer/components/PropsEditor.tsx +1 -1
- package/src/viewer/components/PropsTable.tsx +1 -1
- package/src/viewer/components/RightSidebar.tsx +1 -1
- package/src/viewer/components/ScreenshotButton.tsx +1 -1
- package/src/viewer/components/SkeletonLoader.tsx +1 -1
- package/src/viewer/components/Toast.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/VariantMatrix.tsx +1 -1
- package/src/viewer/components/VariantTabs.tsx +1 -1
- package/src/viewer/components/ViewportSelector.tsx +1 -1
- package/src/viewer/constants/ui.ts +14 -0
- package/src/viewer/entry.tsx +3 -4
- package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
- package/src/viewer/hooks/useViewSettings.ts +1 -2
- package/src/viewer/index.ts +1 -1
- package/src/viewer/preview-frame.html +6 -9
- package/src/viewer/server.ts +106 -9
- package/src/viewer/styles/globals.css +12 -51
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +110 -0
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +89 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +119 -0
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +134 -0
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +66 -0
- package/src/viewer/vendor/shared/src/docs-layout.scss +28 -0
- package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/index.ts +26 -0
- package/src/viewer/vendor/shared/src/types.ts +41 -0
- package/src/viewer/vite-plugin.ts +70 -9
- package/dist/chunk-2EFVPE5Q.js.map +0 -1
- package/dist/chunk-3JPJTU25.js.map +0 -1
- package/dist/chunk-AA6CAHCZ.js.map +0 -1
- package/dist/chunk-CWKQQR6C.js.map +0 -1
- package/dist/init-4VXL3Q6N.js.map +0 -1
- package/dist/viewer-ZWQQ74FV.js.map +0 -1
- /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
- /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
- /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
- /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
- /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
- /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
- /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
- /package/dist/{tokens-M4FCJKBK.js.map → tokens-A3BZIQPB.js.map} +0 -0
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
* Refactored for better performance and maintainability.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useMemo, useEffect, useCallback, useRef, type RefObject } from "react";
|
|
7
|
-
import { BRAND, type FragmentDefinition } from "../../core/index.js";
|
|
6
|
+
import { useState, useMemo, useEffect, useCallback, useRef, type CSSProperties, type ReactNode, type RefObject } from "react";
|
|
7
|
+
import { BRAND, type FragmentDefinition, type FragmentVariant } from "../../core/index.js";
|
|
8
8
|
|
|
9
9
|
// Layout & Navigation
|
|
10
10
|
import { Layout } from "./Layout.js";
|
|
11
11
|
import { LeftSidebar } from "./LeftSidebar.js";
|
|
12
|
-
import { VariantTabs } from "./VariantTabs.js";
|
|
13
12
|
import { CommandPalette } from "./CommandPalette.js";
|
|
14
13
|
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
|
|
15
14
|
import { useToast } from "./Toast.js";
|
|
16
15
|
|
|
17
16
|
// Toolbar
|
|
18
|
-
import { PreviewToolbar
|
|
19
|
-
import {
|
|
17
|
+
import { PreviewToolbar } from "./PreviewToolbar.js";
|
|
18
|
+
import { getBackgroundStyle } from "../constants/ui.js";
|
|
20
19
|
|
|
21
20
|
// Preview & Rendering
|
|
22
21
|
import { PreviewArea } from "./PreviewArea.js";
|
|
@@ -28,10 +27,11 @@ import { useAllFigmaUrls } from "./FigmaEmbed.js";
|
|
|
28
27
|
import { ActionCapture } from "./ActionCapture.js";
|
|
29
28
|
|
|
30
29
|
// Fragments UI
|
|
31
|
-
import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert,
|
|
30
|
+
import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, Input, ThemeToggle } from "@fragments-sdk/ui";
|
|
31
|
+
import { DeviceMobile, GridFour, Rows } from "@phosphor-icons/react";
|
|
32
32
|
|
|
33
33
|
// Icons
|
|
34
|
-
import { EmptyIcon,
|
|
34
|
+
import { EmptyIcon, FigmaIcon, CompareIcon } from "./Icons.js";
|
|
35
35
|
|
|
36
36
|
// Logo
|
|
37
37
|
import { fragmentsLogo } from "../assets/fragments-logo.js";
|
|
@@ -54,9 +54,6 @@ import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useU
|
|
|
54
54
|
import { usePanelDock } from "./ResizablePanel.js";
|
|
55
55
|
import { useTheme } from "./ThemeProvider.js";
|
|
56
56
|
|
|
57
|
-
// Utilities
|
|
58
|
-
import { ScreenshotButton } from "./ScreenshotButton.js";
|
|
59
|
-
|
|
60
57
|
interface AppProps {
|
|
61
58
|
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
62
59
|
}
|
|
@@ -102,6 +99,8 @@ export function App({ fragments }: AppProps) {
|
|
|
102
99
|
}
|
|
103
100
|
return fragments[0]?.path ?? null;
|
|
104
101
|
});
|
|
102
|
+
const activeFragmentPathRef = useRef(activeFragmentPath);
|
|
103
|
+
activeFragmentPathRef.current = activeFragmentPath;
|
|
105
104
|
|
|
106
105
|
const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
|
|
107
106
|
const fragment = fragments.find(s => s.path === activeFragmentPath);
|
|
@@ -118,7 +117,11 @@ export function App({ fragments }: AppProps) {
|
|
|
118
117
|
() => fragments.find((s) => s.path === activeFragmentPath),
|
|
119
118
|
[fragments, activeFragmentPath]
|
|
120
119
|
);
|
|
121
|
-
const
|
|
120
|
+
const variants = activeFragment?.fragment.variants ?? [];
|
|
121
|
+
const variantCount = variants.length;
|
|
122
|
+
const safeVariantIndex = variantCount > 0 ? Math.min(activeVariantIndex, variantCount - 1) : 0;
|
|
123
|
+
const activeVariant = variants[safeVariantIndex];
|
|
124
|
+
const isAllVariantsMode = !urlState.variant;
|
|
122
125
|
const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
|
|
123
126
|
|
|
124
127
|
// Figma integration
|
|
@@ -149,17 +152,34 @@ export function App({ fragments }: AppProps) {
|
|
|
149
152
|
}
|
|
150
153
|
}, [uiState.showComparison, activeVariant, figmaIntegration.extractRenderedStyles, uiState.previewKey]);
|
|
151
154
|
|
|
155
|
+
// Keep focused variant index in range when variant lists change.
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (variantCount === 0) {
|
|
158
|
+
setActiveVariantIndex(0);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (activeVariantIndex >= variantCount) {
|
|
162
|
+
setActiveVariantIndex(variantCount - 1);
|
|
163
|
+
}
|
|
164
|
+
}, [activeVariantIndex, variantCount]);
|
|
165
|
+
|
|
152
166
|
// Sync URL state on browser navigation
|
|
153
167
|
useEffect(() => {
|
|
154
168
|
if (urlState.component) {
|
|
155
169
|
const found = findFragmentByName(fragments, urlState.component);
|
|
156
|
-
if (found
|
|
157
|
-
|
|
170
|
+
if (!found) return;
|
|
171
|
+
|
|
172
|
+
const pathChanged = found.path !== activeFragmentPathRef.current;
|
|
173
|
+
setActiveFragmentPath(found.path);
|
|
174
|
+
uiActions.setHealthDashboard(false);
|
|
175
|
+
|
|
176
|
+
// Keep focused variant when entering "All" on the same component.
|
|
177
|
+
if (urlState.variant || pathChanged) {
|
|
158
178
|
const variantIndex = findVariantIndex(found.fragment.variants, urlState.variant);
|
|
159
179
|
setActiveVariantIndex(variantIndex);
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
|
-
}, [urlState.component, urlState.variant, fragments,
|
|
182
|
+
}, [urlState.component, urlState.variant, fragments, uiActions]);
|
|
163
183
|
|
|
164
184
|
// HMR toast notifications
|
|
165
185
|
useEffect(() => {
|
|
@@ -181,19 +201,64 @@ export function App({ fragments }: AppProps) {
|
|
|
181
201
|
const handleSelectFragment = useCallback((path: string) => {
|
|
182
202
|
const fragment = fragments.find((s) => s.path === path);
|
|
183
203
|
const componentName = fragment?.fragment.meta.name || path;
|
|
184
|
-
const firstVariant = fragment?.fragment.variants?.[0]?.name;
|
|
185
204
|
|
|
186
205
|
setActiveFragmentPath(path);
|
|
187
206
|
setActiveVariantIndex(0);
|
|
188
207
|
uiActions.setHealthDashboard(false);
|
|
189
|
-
setUrlComponent(componentName,
|
|
208
|
+
setUrlComponent(componentName, null);
|
|
190
209
|
}, [fragments, setUrlComponent, uiActions]);
|
|
191
210
|
|
|
211
|
+
const scrollToVariantSection = useCallback(
|
|
212
|
+
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
213
|
+
if (!activeFragment || variantCount === 0) return;
|
|
214
|
+
const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
|
|
215
|
+
const targetVariant = variants[normalizedIndex];
|
|
216
|
+
if (!targetVariant) return;
|
|
217
|
+
|
|
218
|
+
const sectionId = getVariantSectionId(activeFragment.fragment.meta.name, targetVariant.name);
|
|
219
|
+
document.getElementById(sectionId)?.scrollIntoView({ behavior, block: "start" });
|
|
220
|
+
},
|
|
221
|
+
[activeFragment, variantCount, variants]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const focusVariantInAllMode = useCallback(
|
|
225
|
+
(index: number, shouldScroll = false) => {
|
|
226
|
+
if (variantCount === 0) return;
|
|
227
|
+
const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
|
|
228
|
+
setActiveVariantIndex(normalizedIndex);
|
|
229
|
+
if (shouldScroll) {
|
|
230
|
+
scrollToVariantSection(normalizedIndex);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[variantCount, scrollToVariantSection]
|
|
234
|
+
);
|
|
235
|
+
|
|
192
236
|
const handleSelectVariant = useCallback((index: number) => {
|
|
193
|
-
|
|
194
|
-
|
|
237
|
+
if (variantCount === 0) return;
|
|
238
|
+
const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
|
|
239
|
+
const variantName = variants[normalizedIndex]?.name;
|
|
240
|
+
setActiveVariantIndex(normalizedIndex);
|
|
195
241
|
setUrlVariant(variantName || null);
|
|
196
|
-
}, [
|
|
242
|
+
}, [variantCount, variants, setUrlVariant]);
|
|
243
|
+
|
|
244
|
+
const handleSelectAllVariants = useCallback(() => {
|
|
245
|
+
setUrlVariant(null);
|
|
246
|
+
requestAnimationFrame(() => {
|
|
247
|
+
const previewCanvas = document.getElementById("preview-canvas");
|
|
248
|
+
if (previewCanvas instanceof HTMLElement) {
|
|
249
|
+
previewCanvas.scrollTo({ top: 0, behavior: "smooth" });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}, [setUrlVariant]);
|
|
253
|
+
|
|
254
|
+
const handleSelectVariantLink = useCallback((index: number) => {
|
|
255
|
+
if (isAllVariantsMode) {
|
|
256
|
+
// In All mode, selecting a variant link exits All and opens that single variant.
|
|
257
|
+
handleSelectVariant(index);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
handleSelectVariant(index);
|
|
261
|
+
}, [handleSelectVariant, isAllVariantsMode]);
|
|
197
262
|
|
|
198
263
|
// Copy link handler
|
|
199
264
|
const handleCopyLink = useCallback(async () => {
|
|
@@ -219,7 +284,6 @@ export function App({ fragments }: AppProps) {
|
|
|
219
284
|
}, [fragments]);
|
|
220
285
|
|
|
221
286
|
const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || '');
|
|
222
|
-
const variantCount = activeFragment?.fragment.variants?.length || 0;
|
|
223
287
|
|
|
224
288
|
// Keyboard shortcuts
|
|
225
289
|
useKeyboardShortcuts(
|
|
@@ -232,11 +296,36 @@ export function App({ fragments }: AppProps) {
|
|
|
232
296
|
const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
|
|
233
297
|
handleSelectFragment(sortedFragmentPaths[prevIndex]);
|
|
234
298
|
},
|
|
235
|
-
nextVariant: () =>
|
|
236
|
-
|
|
237
|
-
|
|
299
|
+
nextVariant: () => {
|
|
300
|
+
if (variantCount === 0) return;
|
|
301
|
+
const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
|
|
302
|
+
if (isAllVariantsMode) {
|
|
303
|
+
focusVariantInAllMode(nextIndex, true);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
handleSelectVariant(nextIndex);
|
|
307
|
+
},
|
|
308
|
+
prevVariant: () => {
|
|
309
|
+
if (variantCount === 0) return;
|
|
310
|
+
const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
|
|
311
|
+
if (isAllVariantsMode) {
|
|
312
|
+
focusVariantInAllMode(prevIndex, true);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
handleSelectVariant(prevIndex);
|
|
316
|
+
},
|
|
317
|
+
goToVariant: (index) => {
|
|
318
|
+
if (index >= variantCount) return;
|
|
319
|
+
if (isAllVariantsMode) {
|
|
320
|
+
focusVariantInAllMode(index, true);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
handleSelectVariant(index);
|
|
324
|
+
},
|
|
238
325
|
toggleTheme: viewSettings.toggleTheme,
|
|
239
326
|
togglePanel: uiActions.togglePanel,
|
|
327
|
+
toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
|
|
328
|
+
toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
|
|
240
329
|
copyLink: handleCopyLink,
|
|
241
330
|
showHelp: uiActions.toggleShortcutsHelp,
|
|
242
331
|
openSearch: focusSearchInput,
|
|
@@ -256,22 +345,22 @@ export function App({ fragments }: AppProps) {
|
|
|
256
345
|
);
|
|
257
346
|
|
|
258
347
|
// Render variant with action logging via DOM event capture
|
|
259
|
-
const renderVariantWithProps = useCallback(() => {
|
|
260
|
-
if (!
|
|
348
|
+
const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
|
|
349
|
+
if (!variant) return null;
|
|
261
350
|
|
|
262
351
|
return (
|
|
263
352
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
264
|
-
<StoryRenderer variant={
|
|
353
|
+
<StoryRenderer variant={variant}>
|
|
265
354
|
{(content, isLoading, error) => {
|
|
266
355
|
if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
|
|
267
|
-
if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={
|
|
268
|
-
if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={
|
|
356
|
+
if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={variant.name} hint="Check the console for the full error stack trace." />;
|
|
357
|
+
if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={variant.name} hint="The variant's render function didn't return any JSX." />;
|
|
269
358
|
return content;
|
|
270
359
|
}}
|
|
271
360
|
</StoryRenderer>
|
|
272
361
|
</ActionCapture>
|
|
273
362
|
);
|
|
274
|
-
}, [
|
|
363
|
+
}, []);
|
|
275
364
|
|
|
276
365
|
// Check if isolated mode
|
|
277
366
|
const isIsolated = useMemo(() => {
|
|
@@ -302,13 +391,9 @@ export function App({ fragments }: AppProps) {
|
|
|
302
391
|
activeFragment && !uiState.showHealthDashboard ? (
|
|
303
392
|
<TopToolbar
|
|
304
393
|
fragment={activeFragment}
|
|
305
|
-
variant={activeVariant}
|
|
306
|
-
viewSettings={viewSettings}
|
|
307
394
|
uiState={uiState}
|
|
308
395
|
uiActions={uiActions}
|
|
309
396
|
figmaUrl={figmaUrl}
|
|
310
|
-
linkCopied={uiState.linkCopied}
|
|
311
|
-
onCopyLink={handleCopyLink}
|
|
312
397
|
searchQuery={searchQuery}
|
|
313
398
|
onSearchChange={setSearchQuery}
|
|
314
399
|
searchInputRef={searchInputRef}
|
|
@@ -335,6 +420,21 @@ export function App({ fragments }: AppProps) {
|
|
|
335
420
|
}}
|
|
336
421
|
/>
|
|
337
422
|
}
|
|
423
|
+
aside={
|
|
424
|
+
activeFragment && !uiState.showHealthDashboard ? (
|
|
425
|
+
<PreviewAside
|
|
426
|
+
fragment={activeFragment.fragment}
|
|
427
|
+
variants={variants}
|
|
428
|
+
focusedVariantIndex={safeVariantIndex}
|
|
429
|
+
isAllVariantsMode={isAllVariantsMode}
|
|
430
|
+
activePanel={uiState.activePanel}
|
|
431
|
+
onSelectAllVariants={handleSelectAllVariants}
|
|
432
|
+
onSelectVariant={handleSelectVariantLink}
|
|
433
|
+
onCopyLink={handleCopyLink}
|
|
434
|
+
onShowShortcuts={uiActions.toggleShortcutsHelp}
|
|
435
|
+
/>
|
|
436
|
+
) : null
|
|
437
|
+
}
|
|
338
438
|
>
|
|
339
439
|
{uiState.showHealthDashboard ? (
|
|
340
440
|
<div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
|
|
@@ -352,24 +452,25 @@ export function App({ fragments }: AppProps) {
|
|
|
352
452
|
</Box>
|
|
353
453
|
</div>
|
|
354
454
|
) : activeFragment ? (
|
|
355
|
-
<div style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
|
|
455
|
+
<div id="preview-layout" style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
|
|
356
456
|
{/* Main Content Area */}
|
|
357
457
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
458
|
+
<PreviewControlsBar
|
|
459
|
+
zoom={viewSettings.zoom}
|
|
460
|
+
background={viewSettings.background}
|
|
461
|
+
onZoomChange={viewSettings.setZoom}
|
|
462
|
+
onBackgroundChange={viewSettings.setBackground}
|
|
463
|
+
showMatrixView={uiState.showMatrixView}
|
|
464
|
+
showMultiViewport={uiState.showMultiViewport}
|
|
465
|
+
panelOpen={uiState.panelOpen}
|
|
466
|
+
onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
|
|
467
|
+
onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
|
|
468
|
+
onTogglePanel={uiActions.togglePanel}
|
|
469
|
+
/>
|
|
370
470
|
|
|
371
471
|
{/* Preview Area */}
|
|
372
472
|
<div
|
|
473
|
+
id="preview-canvas"
|
|
373
474
|
style={{
|
|
374
475
|
flex: 1,
|
|
375
476
|
overflow: 'auto',
|
|
@@ -377,12 +478,32 @@ export function App({ fragments }: AppProps) {
|
|
|
377
478
|
...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
|
|
378
479
|
}}
|
|
379
480
|
>
|
|
380
|
-
{
|
|
481
|
+
{variantCount === 0 ? (
|
|
482
|
+
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
483
|
+
) : isAllVariantsMode && !uiState.showMatrixView && !uiState.showMultiViewport ? (
|
|
484
|
+
<AllVariantsPreview
|
|
485
|
+
componentName={activeFragment.fragment.meta.name}
|
|
486
|
+
fragmentPath={activeFragment.path}
|
|
487
|
+
variants={variants}
|
|
488
|
+
focusedVariantIndex={safeVariantIndex}
|
|
489
|
+
zoom={viewSettings.zoom}
|
|
490
|
+
background={viewSettings.background}
|
|
491
|
+
viewport={viewSettings.viewport}
|
|
492
|
+
customSize={viewSettings.customSize}
|
|
493
|
+
previewTheme={resolvedTheme}
|
|
494
|
+
showComparison={uiState.showComparison}
|
|
495
|
+
allFigmaUrls={allFigmaUrls}
|
|
496
|
+
fallbackFigmaUrl={activeFragment.fragment.meta.figma}
|
|
497
|
+
onRetry={uiActions.incrementPreviewKey}
|
|
498
|
+
renderVariantContent={renderVariantWithProps}
|
|
499
|
+
previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
|
|
500
|
+
/>
|
|
501
|
+
) : (
|
|
381
502
|
<PreviewArea
|
|
382
503
|
componentName={activeFragment.fragment.meta.name}
|
|
383
504
|
fragmentPath={activeFragment.path}
|
|
384
505
|
variant={activeVariant}
|
|
385
|
-
variants={
|
|
506
|
+
variants={variants}
|
|
386
507
|
zoom={viewSettings.zoom}
|
|
387
508
|
background={viewSettings.background}
|
|
388
509
|
viewport={viewSettings.viewport}
|
|
@@ -398,40 +519,40 @@ export function App({ fragments }: AppProps) {
|
|
|
398
519
|
handleSelectVariant(index);
|
|
399
520
|
}}
|
|
400
521
|
onRetry={uiActions.incrementPreviewKey}
|
|
401
|
-
renderContent={renderVariantWithProps}
|
|
402
|
-
previewKey={`${activeFragmentPath}-${
|
|
522
|
+
renderContent={() => renderVariantWithProps(activeVariant)}
|
|
523
|
+
previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
|
|
403
524
|
/>
|
|
404
|
-
) : (
|
|
405
|
-
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
406
525
|
)}
|
|
407
526
|
</div>
|
|
408
527
|
</div>
|
|
409
528
|
|
|
410
529
|
{/* Bottom Panel */}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
530
|
+
<div id="preview-tools">
|
|
531
|
+
{uiState.panelOpen && activeVariant && (
|
|
532
|
+
<BottomPanel
|
|
533
|
+
fragment={activeFragment.fragment}
|
|
534
|
+
variant={activeVariant}
|
|
535
|
+
fragments={fragments}
|
|
536
|
+
activePanel={uiState.activePanel}
|
|
537
|
+
onPanelChange={uiActions.setActivePanel}
|
|
538
|
+
figmaUrl={figmaUrl}
|
|
539
|
+
figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
|
|
540
|
+
renderedStyles={figmaIntegration.renderedStyles}
|
|
541
|
+
figmaLoading={figmaIntegration.isLoading}
|
|
542
|
+
figmaError={figmaIntegration.errorMessage}
|
|
543
|
+
onFetchFigma={figmaIntegration.fetchFigmaStyles}
|
|
544
|
+
onRefreshRendered={figmaIntegration.extractRenderedStyles}
|
|
545
|
+
actionLogs={actionLogs}
|
|
546
|
+
onClearActionLogs={clearActionLogs}
|
|
547
|
+
onNavigateToComponent={(name) => {
|
|
548
|
+
const target = fragments.find(s => s.fragment.meta.name === name);
|
|
549
|
+
if (target) handleSelectFragment(target.path);
|
|
550
|
+
}}
|
|
551
|
+
previewKey={uiState.previewKey}
|
|
552
|
+
fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
|
|
553
|
+
/>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
435
556
|
</div>
|
|
436
557
|
) : (
|
|
437
558
|
<EmptyState style={{ height: '100%' }}>
|
|
@@ -450,13 +571,9 @@ export function App({ fragments }: AppProps) {
|
|
|
450
571
|
// Top Toolbar Component
|
|
451
572
|
interface TopToolbarProps {
|
|
452
573
|
fragment: { path: string; fragment: FragmentDefinition };
|
|
453
|
-
variant: any;
|
|
454
|
-
viewSettings: ReturnType<typeof useViewSettings>;
|
|
455
574
|
uiState: ReturnType<typeof useAppState>['state'];
|
|
456
575
|
uiActions: ReturnType<typeof useAppState>['actions'];
|
|
457
576
|
figmaUrl?: string;
|
|
458
|
-
linkCopied: boolean;
|
|
459
|
-
onCopyLink: () => void;
|
|
460
577
|
searchQuery: string;
|
|
461
578
|
onSearchChange: (value: string) => void;
|
|
462
579
|
searchInputRef: RefObject<HTMLInputElement>;
|
|
@@ -475,6 +592,18 @@ interface HeaderSearchProps {
|
|
|
475
592
|
inputRef: RefObject<HTMLInputElement>;
|
|
476
593
|
}
|
|
477
594
|
|
|
595
|
+
interface PreviewAsideProps {
|
|
596
|
+
fragment: FragmentDefinition;
|
|
597
|
+
variants: FragmentVariant[];
|
|
598
|
+
focusedVariantIndex: number;
|
|
599
|
+
isAllVariantsMode: boolean;
|
|
600
|
+
activePanel: string;
|
|
601
|
+
onSelectAllVariants: () => void;
|
|
602
|
+
onSelectVariant: (index: number) => void;
|
|
603
|
+
onCopyLink: () => void;
|
|
604
|
+
onShowShortcuts: () => void;
|
|
605
|
+
}
|
|
606
|
+
|
|
478
607
|
function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
479
608
|
return (
|
|
480
609
|
<Header.Search expandable>
|
|
@@ -536,15 +665,113 @@ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef
|
|
|
536
665
|
);
|
|
537
666
|
}
|
|
538
667
|
|
|
668
|
+
function PreviewAside({
|
|
669
|
+
fragment,
|
|
670
|
+
variants,
|
|
671
|
+
focusedVariantIndex,
|
|
672
|
+
isAllVariantsMode,
|
|
673
|
+
activePanel,
|
|
674
|
+
onSelectAllVariants,
|
|
675
|
+
onSelectVariant,
|
|
676
|
+
onCopyLink,
|
|
677
|
+
onShowShortcuts,
|
|
678
|
+
}: PreviewAsideProps) {
|
|
679
|
+
const focusedVariant = variants[focusedVariantIndex] || null;
|
|
680
|
+
|
|
681
|
+
const baseLinkStyle: CSSProperties = {
|
|
682
|
+
color: 'var(--text-secondary)',
|
|
683
|
+
textDecoration: 'none',
|
|
684
|
+
fontSize: '13px',
|
|
685
|
+
borderRadius: '6px',
|
|
686
|
+
padding: '4px 8px',
|
|
687
|
+
display: 'block',
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const getLinkStyle = (isActive = false): CSSProperties => (
|
|
691
|
+
isActive
|
|
692
|
+
? { ...baseLinkStyle, color: 'var(--text-primary)', backgroundColor: 'var(--bg-hover)' }
|
|
693
|
+
: baseLinkStyle
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
<Box padding="md" style={{ position: 'sticky', top: '80px' }}>
|
|
698
|
+
<Stack gap="md">
|
|
699
|
+
<Stack gap="xs">
|
|
700
|
+
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em' }}>
|
|
701
|
+
On this page
|
|
702
|
+
</Text>
|
|
703
|
+
<a href="#preview-canvas" style={baseLinkStyle}>
|
|
704
|
+
Preview
|
|
705
|
+
</a>
|
|
706
|
+
<a href="#preview-tools" style={baseLinkStyle}>
|
|
707
|
+
Panels
|
|
708
|
+
</a>
|
|
709
|
+
{variants.length > 0 && (
|
|
710
|
+
<>
|
|
711
|
+
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '8px' }}>
|
|
712
|
+
Variants
|
|
713
|
+
</Text>
|
|
714
|
+
<a
|
|
715
|
+
href="#preview-canvas"
|
|
716
|
+
style={getLinkStyle(isAllVariantsMode)}
|
|
717
|
+
onClick={(event) => {
|
|
718
|
+
event.preventDefault();
|
|
719
|
+
onSelectAllVariants();
|
|
720
|
+
}}
|
|
721
|
+
>
|
|
722
|
+
All
|
|
723
|
+
</a>
|
|
724
|
+
{variants.map((variant, index) => {
|
|
725
|
+
const active = index === focusedVariantIndex;
|
|
726
|
+
|
|
727
|
+
return (
|
|
728
|
+
<a
|
|
729
|
+
key={variant.name}
|
|
730
|
+
href="#preview-canvas"
|
|
731
|
+
style={getLinkStyle(active)}
|
|
732
|
+
onClick={(event) => {
|
|
733
|
+
event.preventDefault();
|
|
734
|
+
onSelectVariant(index);
|
|
735
|
+
}}
|
|
736
|
+
>
|
|
737
|
+
{variant.name}
|
|
738
|
+
</a>
|
|
739
|
+
);
|
|
740
|
+
})}
|
|
741
|
+
</>
|
|
742
|
+
)}
|
|
743
|
+
</Stack>
|
|
744
|
+
<Separator />
|
|
745
|
+
<Stack gap="xs">
|
|
746
|
+
<Text size="sm" weight="medium">{fragment.meta.name}</Text>
|
|
747
|
+
<Text size="xs" color="secondary">
|
|
748
|
+
{isAllVariantsMode
|
|
749
|
+
? `Variant: All${focusedVariant ? ` (focused: ${focusedVariant.name})` : ''}`
|
|
750
|
+
: focusedVariant ? `Variant: ${focusedVariant.name}` : 'Select a variant'}
|
|
751
|
+
</Text>
|
|
752
|
+
<Text size="xs" color="tertiary">
|
|
753
|
+
Active panel: {activePanel}
|
|
754
|
+
</Text>
|
|
755
|
+
</Stack>
|
|
756
|
+
<Separator />
|
|
757
|
+
<Stack gap="xs">
|
|
758
|
+
<Button variant="ghost" size="sm" onClick={onCopyLink}>
|
|
759
|
+
Copy Link
|
|
760
|
+
</Button>
|
|
761
|
+
<Button variant="ghost" size="sm" onClick={onShowShortcuts}>
|
|
762
|
+
Keyboard Shortcuts
|
|
763
|
+
</Button>
|
|
764
|
+
</Stack>
|
|
765
|
+
</Stack>
|
|
766
|
+
</Box>
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
539
770
|
function TopToolbar({
|
|
540
771
|
fragment,
|
|
541
|
-
variant,
|
|
542
|
-
viewSettings,
|
|
543
772
|
uiState,
|
|
544
773
|
uiActions,
|
|
545
774
|
figmaUrl,
|
|
546
|
-
linkCopied,
|
|
547
|
-
onCopyLink,
|
|
548
775
|
searchQuery,
|
|
549
776
|
onSearchChange,
|
|
550
777
|
searchInputRef,
|
|
@@ -563,21 +790,6 @@ function TopToolbar({
|
|
|
563
790
|
<HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
|
|
564
791
|
<Header.Spacer />
|
|
565
792
|
<Header.Actions>
|
|
566
|
-
<PreviewToolbar
|
|
567
|
-
zoom={viewSettings.zoom}
|
|
568
|
-
background={viewSettings.background}
|
|
569
|
-
onZoomChange={viewSettings.setZoom}
|
|
570
|
-
onBackgroundChange={viewSettings.setBackground}
|
|
571
|
-
/>
|
|
572
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
573
|
-
<ViewportSelector
|
|
574
|
-
viewport={viewSettings.viewport}
|
|
575
|
-
customSize={viewSettings.customSize}
|
|
576
|
-
onViewportChange={viewSettings.setViewport}
|
|
577
|
-
onCustomSizeChange={viewSettings.setCustomSize}
|
|
578
|
-
/>
|
|
579
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
580
|
-
|
|
581
793
|
{figmaUrl && (
|
|
582
794
|
<>
|
|
583
795
|
<Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
|
|
@@ -602,41 +814,6 @@ function TopToolbar({
|
|
|
602
814
|
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
603
815
|
</>
|
|
604
816
|
)}
|
|
605
|
-
|
|
606
|
-
{variant && (
|
|
607
|
-
<>
|
|
608
|
-
<Tooltip content="Open in new window">
|
|
609
|
-
<Button
|
|
610
|
-
onClick={() => {
|
|
611
|
-
const url = new URL(window.location.href);
|
|
612
|
-
url.hash = '';
|
|
613
|
-
url.searchParams.set('isolated', 'true');
|
|
614
|
-
url.searchParams.set('component', fragment.fragment.meta.name);
|
|
615
|
-
url.searchParams.set('variant', variant.name);
|
|
616
|
-
if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
|
|
617
|
-
if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
|
|
618
|
-
window.open(url.toString(), '_blank', 'noopener,noreferrer');
|
|
619
|
-
}}
|
|
620
|
-
variant="ghost"
|
|
621
|
-
size="sm"
|
|
622
|
-
>
|
|
623
|
-
<ExternalLinkIcon style={{ width: '16px', height: '16px' }} />
|
|
624
|
-
</Button>
|
|
625
|
-
</Tooltip>
|
|
626
|
-
<ScreenshotButton componentName={fragment.fragment.meta.name} variantName={variant.name} />
|
|
627
|
-
<Tooltip content="Copy link to share">
|
|
628
|
-
<Button
|
|
629
|
-
onClick={onCopyLink}
|
|
630
|
-
variant="ghost"
|
|
631
|
-
size="sm"
|
|
632
|
-
style={linkCopied ? { color: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.1)' } : {}}
|
|
633
|
-
>
|
|
634
|
-
{linkCopied ? <CheckIcon style={{ width: '16px', height: '16px' }} /> : <LinkIcon style={{ width: '16px', height: '16px' }} />}
|
|
635
|
-
</Button>
|
|
636
|
-
</Tooltip>
|
|
637
|
-
</>
|
|
638
|
-
)}
|
|
639
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
640
817
|
<ThemeToggle
|
|
641
818
|
size="sm"
|
|
642
819
|
value={resolvedTheme}
|
|
@@ -666,52 +843,180 @@ function TopToolbar({
|
|
|
666
843
|
);
|
|
667
844
|
}
|
|
668
845
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
846
|
+
function normalizeAnchorSegment(value: string): string {
|
|
847
|
+
const normalized = value
|
|
848
|
+
.toLowerCase()
|
|
849
|
+
.trim()
|
|
850
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
851
|
+
.replace(/^-+|-+$/g, "");
|
|
852
|
+
return normalized || "variant";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function getVariantSectionId(componentName: string, variantName: string): string {
|
|
856
|
+
return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
interface PreviewControlsBarProps {
|
|
860
|
+
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
861
|
+
background: ReturnType<typeof useViewSettings>["background"];
|
|
862
|
+
onZoomChange: ReturnType<typeof useViewSettings>["setZoom"];
|
|
863
|
+
onBackgroundChange: ReturnType<typeof useViewSettings>["setBackground"];
|
|
674
864
|
showMatrixView: boolean;
|
|
675
865
|
showMultiViewport: boolean;
|
|
866
|
+
panelOpen: boolean;
|
|
676
867
|
onToggleMatrix: () => void;
|
|
677
868
|
onToggleMultiViewport: () => void;
|
|
869
|
+
onTogglePanel: () => void;
|
|
678
870
|
}
|
|
679
871
|
|
|
680
|
-
function
|
|
872
|
+
function PreviewControlsBar({
|
|
873
|
+
zoom,
|
|
874
|
+
background,
|
|
875
|
+
onZoomChange,
|
|
876
|
+
onBackgroundChange,
|
|
877
|
+
showMatrixView,
|
|
878
|
+
showMultiViewport,
|
|
879
|
+
panelOpen,
|
|
880
|
+
onToggleMatrix,
|
|
881
|
+
onToggleMultiViewport,
|
|
882
|
+
onTogglePanel,
|
|
883
|
+
}: PreviewControlsBarProps) {
|
|
884
|
+
const toggleButtonStyle = (active: boolean): CSSProperties => (
|
|
885
|
+
active ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}
|
|
886
|
+
);
|
|
887
|
+
|
|
681
888
|
return (
|
|
682
|
-
<
|
|
683
|
-
|
|
684
|
-
<
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
{
|
|
889
|
+
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
|
|
890
|
+
<Stack direction="row" gap="sm" align="center" justify="end">
|
|
891
|
+
<PreviewToolbar
|
|
892
|
+
zoom={zoom}
|
|
893
|
+
background={background}
|
|
894
|
+
onZoomChange={onZoomChange}
|
|
895
|
+
onBackgroundChange={onBackgroundChange}
|
|
896
|
+
/>
|
|
897
|
+
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
898
|
+
<Tooltip content={showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
|
|
692
899
|
<Button
|
|
900
|
+
variant="ghost"
|
|
901
|
+
size="sm"
|
|
902
|
+
aria-pressed={showMatrixView}
|
|
903
|
+
aria-label="Toggle matrix view"
|
|
693
904
|
onClick={onToggleMatrix}
|
|
905
|
+
style={toggleButtonStyle(showMatrixView)}
|
|
906
|
+
>
|
|
907
|
+
<GridFour size={16} />
|
|
908
|
+
</Button>
|
|
909
|
+
</Tooltip>
|
|
910
|
+
<Tooltip content={showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
|
|
911
|
+
<Button
|
|
694
912
|
variant="ghost"
|
|
695
913
|
size="sm"
|
|
696
|
-
|
|
697
|
-
|
|
914
|
+
aria-pressed={showMultiViewport}
|
|
915
|
+
aria-label="Toggle responsive view"
|
|
916
|
+
onClick={onToggleMultiViewport}
|
|
917
|
+
style={toggleButtonStyle(showMultiViewport)}
|
|
698
918
|
>
|
|
699
|
-
<
|
|
700
|
-
{showMatrixView ? "Exit Matrix" : "Matrix"}
|
|
919
|
+
<DeviceMobile size={16} />
|
|
701
920
|
</Button>
|
|
702
|
-
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
921
|
+
</Tooltip>
|
|
922
|
+
<Tooltip content={panelOpen ? "Hide addons panel" : "Show addons panel"}>
|
|
923
|
+
<Button
|
|
924
|
+
variant="ghost"
|
|
925
|
+
size="sm"
|
|
926
|
+
aria-pressed={panelOpen}
|
|
927
|
+
aria-label="Toggle addons panel"
|
|
928
|
+
onClick={onTogglePanel}
|
|
929
|
+
style={toggleButtonStyle(panelOpen)}
|
|
930
|
+
>
|
|
931
|
+
<Rows size={16} />
|
|
932
|
+
</Button>
|
|
933
|
+
</Tooltip>
|
|
713
934
|
</Stack>
|
|
714
|
-
</
|
|
935
|
+
</div>
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
interface AllVariantsPreviewProps {
|
|
940
|
+
componentName: string;
|
|
941
|
+
fragmentPath: string;
|
|
942
|
+
variants: FragmentVariant[];
|
|
943
|
+
focusedVariantIndex: number;
|
|
944
|
+
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
945
|
+
background: ReturnType<typeof useViewSettings>["background"];
|
|
946
|
+
viewport: ReturnType<typeof useViewSettings>["viewport"];
|
|
947
|
+
customSize: ReturnType<typeof useViewSettings>["customSize"];
|
|
948
|
+
previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
|
|
949
|
+
showComparison: boolean;
|
|
950
|
+
allFigmaUrls: string[];
|
|
951
|
+
fallbackFigmaUrl?: string;
|
|
952
|
+
onRetry: () => void;
|
|
953
|
+
renderVariantContent: (variant: FragmentVariant) => ReactNode;
|
|
954
|
+
previewKeyBase: string;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function AllVariantsPreview({
|
|
958
|
+
componentName,
|
|
959
|
+
fragmentPath,
|
|
960
|
+
variants,
|
|
961
|
+
focusedVariantIndex,
|
|
962
|
+
zoom,
|
|
963
|
+
background,
|
|
964
|
+
viewport,
|
|
965
|
+
customSize,
|
|
966
|
+
previewTheme,
|
|
967
|
+
showComparison,
|
|
968
|
+
allFigmaUrls,
|
|
969
|
+
fallbackFigmaUrl,
|
|
970
|
+
onRetry,
|
|
971
|
+
renderVariantContent,
|
|
972
|
+
previewKeyBase,
|
|
973
|
+
}: AllVariantsPreviewProps) {
|
|
974
|
+
return (
|
|
975
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
|
|
976
|
+
{variants.map((variant, index) => {
|
|
977
|
+
const isFocused = index === focusedVariantIndex;
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<section
|
|
981
|
+
id={getVariantSectionId(componentName, variant.name)}
|
|
982
|
+
key={variant.name}
|
|
983
|
+
style={{
|
|
984
|
+
border: '1px solid var(--border)',
|
|
985
|
+
borderColor: isFocused ? 'var(--color-accent)' : 'var(--border)',
|
|
986
|
+
borderRadius: '10px',
|
|
987
|
+
overflow: 'hidden',
|
|
988
|
+
backgroundColor: 'var(--bg-primary)',
|
|
989
|
+
boxShadow: isFocused ? '0 0 0 1px color-mix(in srgb, var(--color-accent) 40%, transparent)' : undefined,
|
|
990
|
+
}}
|
|
991
|
+
>
|
|
992
|
+
<Stack direction="row" align="baseline" gap="sm" style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
|
|
993
|
+
<Text size="sm" weight="medium">{variant.name}</Text>
|
|
994
|
+
<Text size="xs" color="secondary">{variant.description}</Text>
|
|
995
|
+
</Stack>
|
|
996
|
+
<PreviewArea
|
|
997
|
+
componentName={componentName}
|
|
998
|
+
fragmentPath={fragmentPath}
|
|
999
|
+
variant={variant}
|
|
1000
|
+
variants={variants}
|
|
1001
|
+
zoom={zoom}
|
|
1002
|
+
background={background}
|
|
1003
|
+
viewport={viewport}
|
|
1004
|
+
customSize={customSize}
|
|
1005
|
+
previewTheme={previewTheme}
|
|
1006
|
+
showMatrixView={false}
|
|
1007
|
+
showMultiViewport={false}
|
|
1008
|
+
showComparison={showComparison}
|
|
1009
|
+
figmaUrl={variant.figma || fallbackFigmaUrl}
|
|
1010
|
+
allFigmaUrls={allFigmaUrls}
|
|
1011
|
+
onSelectVariant={() => {}}
|
|
1012
|
+
onRetry={onRetry}
|
|
1013
|
+
renderContent={() => renderVariantContent(variant)}
|
|
1014
|
+
previewKey={`${previewKeyBase}-${index}`}
|
|
1015
|
+
/>
|
|
1016
|
+
</section>
|
|
1017
|
+
);
|
|
1018
|
+
})}
|
|
1019
|
+
</div>
|
|
715
1020
|
);
|
|
716
1021
|
}
|
|
717
1022
|
|
|
@@ -721,6 +1026,12 @@ interface NoVariantsMessageProps {
|
|
|
721
1026
|
}
|
|
722
1027
|
|
|
723
1028
|
function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
|
|
1029
|
+
// Check for load error (missing dependencies, schema errors, etc.)
|
|
1030
|
+
const loadError = (fragment as any)?._loadError;
|
|
1031
|
+
if (loadError) {
|
|
1032
|
+
return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
724
1035
|
const skippedVariants = (fragment?._generated as any)?.skippedVariants;
|
|
725
1036
|
|
|
726
1037
|
if (!skippedVariants || skippedVariants.length === 0) {
|
|
@@ -761,6 +1072,92 @@ function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
|
|
|
761
1072
|
);
|
|
762
1073
|
}
|
|
763
1074
|
|
|
1075
|
+
// Load error message — shown when a fragment failed to import (missing deps, schema errors, etc.)
|
|
1076
|
+
function LoadErrorMessage({ error, componentName }: { error: { message: string; dependencies: string[] }; componentName?: string }) {
|
|
1077
|
+
const deps = error.dependencies || [];
|
|
1078
|
+
const errorMessage = error.message || 'Unknown error';
|
|
1079
|
+
|
|
1080
|
+
// Determine if the error is a missing module/dependency issue
|
|
1081
|
+
const isModuleError = /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(errorMessage);
|
|
1082
|
+
|
|
1083
|
+
// Only suggest packages if the error is actually about missing modules
|
|
1084
|
+
let suggestedPackages: string[] = [];
|
|
1085
|
+
if (isModuleError) {
|
|
1086
|
+
if (deps.length > 0) {
|
|
1087
|
+
suggestedPackages = [...deps];
|
|
1088
|
+
} else {
|
|
1089
|
+
const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
|
|
1090
|
+
if (match) {
|
|
1091
|
+
suggestedPackages = [match[1]];
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const hasMissingDeps = suggestedPackages.length > 0;
|
|
1097
|
+
const installCmd = `pnpm add ${suggestedPackages.join(' ')}`;
|
|
1098
|
+
|
|
1099
|
+
return (
|
|
1100
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
|
|
1101
|
+
<Alert variant="warning">
|
|
1102
|
+
<Alert.Body>
|
|
1103
|
+
<Alert.Title>
|
|
1104
|
+
{hasMissingDeps ? 'Missing Dependencies' : 'Failed to Load'}
|
|
1105
|
+
</Alert.Title>
|
|
1106
|
+
<Alert.Content>
|
|
1107
|
+
<Stack direction="column" gap="sm">
|
|
1108
|
+
{hasMissingDeps ? (
|
|
1109
|
+
<>
|
|
1110
|
+
<Text size="xs" color="secondary">
|
|
1111
|
+
{componentName ? `${componentName} requires` : 'This component requires'} packages that are not installed in your project.
|
|
1112
|
+
</Text>
|
|
1113
|
+
<Text size="xs" weight="semibold" color="secondary">Install with:</Text>
|
|
1114
|
+
<code style={{
|
|
1115
|
+
display: 'block',
|
|
1116
|
+
padding: '8px 12px',
|
|
1117
|
+
borderRadius: '6px',
|
|
1118
|
+
backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
|
|
1119
|
+
fontFamily: 'monospace',
|
|
1120
|
+
fontSize: '12px',
|
|
1121
|
+
color: 'var(--text-primary, #111827)',
|
|
1122
|
+
userSelect: 'all',
|
|
1123
|
+
}}>
|
|
1124
|
+
{installCmd}
|
|
1125
|
+
</code>
|
|
1126
|
+
<Text size="xs" color="tertiary">
|
|
1127
|
+
After installing, restart the dev server.
|
|
1128
|
+
</Text>
|
|
1129
|
+
</>
|
|
1130
|
+
) : (
|
|
1131
|
+
<>
|
|
1132
|
+
<Text size="xs" color="secondary">
|
|
1133
|
+
{componentName ? `${componentName} couldn't` : 'This component couldn\'t'} be loaded. This may be due to a schema validation error or missing imports.
|
|
1134
|
+
</Text>
|
|
1135
|
+
<Text size="xs" weight="semibold" color="secondary">Error:</Text>
|
|
1136
|
+
<pre style={{
|
|
1137
|
+
padding: '8px 12px',
|
|
1138
|
+
borderRadius: '6px',
|
|
1139
|
+
backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
|
|
1140
|
+
fontFamily: 'monospace',
|
|
1141
|
+
fontSize: '11px',
|
|
1142
|
+
color: 'var(--text-secondary, #374151)',
|
|
1143
|
+
whiteSpace: 'pre-wrap',
|
|
1144
|
+
wordBreak: 'break-word',
|
|
1145
|
+
margin: 0,
|
|
1146
|
+
maxHeight: '200px',
|
|
1147
|
+
overflow: 'auto',
|
|
1148
|
+
}}>
|
|
1149
|
+
{errorMessage}
|
|
1150
|
+
</pre>
|
|
1151
|
+
</>
|
|
1152
|
+
)}
|
|
1153
|
+
</Stack>
|
|
1154
|
+
</Alert.Content>
|
|
1155
|
+
</Alert.Body>
|
|
1156
|
+
</Alert>
|
|
1157
|
+
</div>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
764
1161
|
// Empty variant message
|
|
765
1162
|
interface EmptyVariantMessageProps {
|
|
766
1163
|
reason: string;
|