@fragments-sdk/cli 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +83 -33
- 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-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
- 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-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +1 -1
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/generate.ts +383 -68
- 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 +169 -2
- 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/DocsHeaderBar.tsx +6 -18
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
- 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/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-KSAAS7X3.js.map +0 -1
- package/dist/viewer-SBTJDMP7.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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Refactored for better performance and maintainability.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useMemo, useEffect, useCallback, useRef, type
|
|
7
|
-
import {
|
|
6
|
+
import { useState, useMemo, useEffect, useCallback, useRef, type ReactNode } from "react";
|
|
7
|
+
import type { FragmentDefinition, FragmentVariant } from "../../core/index.js";
|
|
8
8
|
|
|
9
9
|
// Layout & Navigation
|
|
10
10
|
import { Layout } from "./Layout.js";
|
|
@@ -13,8 +13,9 @@ import { CommandPalette } from "./CommandPalette.js";
|
|
|
13
13
|
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
|
|
14
14
|
import { useToast } from "./Toast.js";
|
|
15
15
|
|
|
16
|
-
// Toolbar
|
|
17
|
-
import {
|
|
16
|
+
// Toolbar & Header
|
|
17
|
+
import { TopToolbar } from "./TopToolbar.js";
|
|
18
|
+
import { ViewerHeader } from "./ViewerHeader.js";
|
|
18
19
|
|
|
19
20
|
// Preview & Rendering
|
|
20
21
|
import { PreviewArea } from "./PreviewArea.js";
|
|
@@ -25,21 +26,21 @@ import { HealthDashboard } from "./HealthDashboard.js";
|
|
|
25
26
|
import { useAllFigmaUrls } from "./FigmaEmbed.js";
|
|
26
27
|
import { ActionCapture } from "./ActionCapture.js";
|
|
27
28
|
|
|
29
|
+
// Extracted sub-components
|
|
30
|
+
import { PreviewAside } from "./PreviewAside.js";
|
|
31
|
+
import { AllVariantsPreview } from "./AllVariantsPreview.js";
|
|
32
|
+
import { ComponentDocView } from "./ComponentDocView.js";
|
|
33
|
+
import { NoVariantsMessage } from "./NoVariantsMessage.js";
|
|
34
|
+
import { EmptyVariantMessage } from "./EmptyVariantMessage.js";
|
|
35
|
+
|
|
28
36
|
// Fragments UI
|
|
29
|
-
import {
|
|
30
|
-
import { DeviceMobile, GridFour, Rows } from "@phosphor-icons/react";
|
|
37
|
+
import { Stack, Box, EmptyState } from "@fragments-sdk/ui";
|
|
31
38
|
|
|
32
39
|
// Icons
|
|
33
|
-
import { EmptyIcon
|
|
34
|
-
|
|
40
|
+
import { EmptyIcon } from "./Icons.js";
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
39
|
-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
40
|
-
</svg>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
42
|
+
// Utilities
|
|
43
|
+
import { getVariantSectionId } from "./viewer-utils.js";
|
|
43
44
|
|
|
44
45
|
// Hooks
|
|
45
46
|
import { useAppState } from "../hooks/useAppState.js";
|
|
@@ -48,9 +49,7 @@ import { useFigmaIntegration } from "../hooks/useFigmaIntegration.js";
|
|
|
48
49
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
|
|
49
50
|
import { useActions } from "../hooks/useActions.js";
|
|
50
51
|
import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useUrlState.js";
|
|
51
|
-
import { usePanelDock } from "./ResizablePanel.js";
|
|
52
52
|
import { useTheme } from "./ThemeProvider.js";
|
|
53
|
-
import { WebMCPStatusIndicator } from "./WebMCPStatusIndicator.js";
|
|
54
53
|
import { ViewerStateSync } from "./ViewerStateSync.js";
|
|
55
54
|
|
|
56
55
|
interface AppProps {
|
|
@@ -59,7 +58,13 @@ interface AppProps {
|
|
|
59
58
|
|
|
60
59
|
export function App({ fragments }: AppProps) {
|
|
61
60
|
// URL state management
|
|
62
|
-
const {
|
|
61
|
+
const {
|
|
62
|
+
state: urlState,
|
|
63
|
+
setComponent: setUrlComponent,
|
|
64
|
+
setVariant: setUrlVariant,
|
|
65
|
+
setViewSettings: setUrlViewSettings,
|
|
66
|
+
copyUrl,
|
|
67
|
+
} = useUrlState();
|
|
63
68
|
|
|
64
69
|
// UI state (modals, panels, view modes)
|
|
65
70
|
const { state: uiState, actions: uiActions } = useAppState();
|
|
@@ -72,16 +77,14 @@ export function App({ fragments }: AppProps) {
|
|
|
72
77
|
customSize: { width: urlState.customWidth, height: urlState.customHeight },
|
|
73
78
|
},
|
|
74
79
|
onZoomChange: (zoom) => setUrlViewSettings({ zoom }),
|
|
75
|
-
onViewportChange: (vp, size) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
onViewportChange: (vp, size) =>
|
|
81
|
+
setUrlViewSettings({
|
|
82
|
+
viewport: vp,
|
|
83
|
+
customWidth: size?.width,
|
|
84
|
+
customHeight: size?.height,
|
|
85
|
+
}),
|
|
80
86
|
});
|
|
81
87
|
|
|
82
|
-
// Panel dock position
|
|
83
|
-
const panelDock = usePanelDock();
|
|
84
|
-
|
|
85
88
|
// Get resolved theme from ThemeProvider for iframe preview
|
|
86
89
|
const { resolvedTheme } = useTheme();
|
|
87
90
|
|
|
@@ -100,13 +103,13 @@ export function App({ fragments }: AppProps) {
|
|
|
100
103
|
activeFragmentPathRef.current = activeFragmentPath;
|
|
101
104
|
|
|
102
105
|
const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
|
|
103
|
-
const fragment = fragments.find(s => s.path === activeFragmentPath);
|
|
106
|
+
const fragment = fragments.find((s) => s.path === activeFragmentPath);
|
|
104
107
|
if (urlState.variant && fragment?.fragment.variants) {
|
|
105
108
|
return findVariantIndex(fragment.fragment.variants, urlState.variant);
|
|
106
109
|
}
|
|
107
110
|
return 0;
|
|
108
111
|
});
|
|
109
|
-
const [searchQuery, setSearchQuery] = useState(
|
|
112
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
110
113
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
111
114
|
|
|
112
115
|
// Derived values
|
|
@@ -118,7 +121,6 @@ export function App({ fragments }: AppProps) {
|
|
|
118
121
|
const variantCount = variants.length;
|
|
119
122
|
const safeVariantIndex = variantCount > 0 ? Math.min(activeVariantIndex, variantCount - 1) : 0;
|
|
120
123
|
const activeVariant = variants[safeVariantIndex];
|
|
121
|
-
const isAllVariantsMode = !urlState.variant;
|
|
122
124
|
const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
|
|
123
125
|
|
|
124
126
|
// Figma integration
|
|
@@ -147,7 +149,12 @@ export function App({ fragments }: AppProps) {
|
|
|
147
149
|
const timer = setTimeout(figmaIntegration.extractRenderedStyles, 100);
|
|
148
150
|
return () => clearTimeout(timer);
|
|
149
151
|
}
|
|
150
|
-
}, [
|
|
152
|
+
}, [
|
|
153
|
+
uiState.showComparison,
|
|
154
|
+
activeVariant,
|
|
155
|
+
figmaIntegration.extractRenderedStyles,
|
|
156
|
+
uiState.previewKey,
|
|
157
|
+
]);
|
|
151
158
|
|
|
152
159
|
// Keep focused variant index in range when variant lists change.
|
|
153
160
|
useEffect(() => {
|
|
@@ -185,25 +192,28 @@ export function App({ fragments }: AppProps) {
|
|
|
185
192
|
|
|
186
193
|
const handleUpdate = (data: any) => {
|
|
187
194
|
if (data?.updates?.length > 0) {
|
|
188
|
-
const paths = data.updates.map((u: any) => u.path.split(
|
|
189
|
-
info(
|
|
195
|
+
const paths = data.updates.map((u: any) => u.path.split("/").pop()).join(", ");
|
|
196
|
+
info("HMR Update", `Updated: ${paths}`);
|
|
190
197
|
}
|
|
191
198
|
};
|
|
192
199
|
|
|
193
|
-
hot.on(
|
|
194
|
-
return () => hot.off?.(
|
|
200
|
+
hot.on("vite:beforeUpdate", handleUpdate);
|
|
201
|
+
return () => hot.off?.("vite:beforeUpdate", handleUpdate);
|
|
195
202
|
}, [info]);
|
|
196
203
|
|
|
197
204
|
// Navigation handlers
|
|
198
|
-
const handleSelectFragment = useCallback(
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
const handleSelectFragment = useCallback(
|
|
206
|
+
(path: string) => {
|
|
207
|
+
const fragment = fragments.find((s) => s.path === path);
|
|
208
|
+
const componentName = fragment?.fragment.meta.name || path;
|
|
201
209
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
setActiveFragmentPath(path);
|
|
211
|
+
setActiveVariantIndex(0);
|
|
212
|
+
uiActions.setHealthDashboard(false);
|
|
213
|
+
setUrlComponent(componentName, null);
|
|
214
|
+
},
|
|
215
|
+
[fragments, setUrlComponent, uiActions]
|
|
216
|
+
);
|
|
207
217
|
|
|
208
218
|
const scrollToVariantSection = useCallback(
|
|
209
219
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
@@ -230,39 +240,31 @@ export function App({ fragments }: AppProps) {
|
|
|
230
240
|
[variantCount, scrollToVariantSection]
|
|
231
241
|
);
|
|
232
242
|
|
|
233
|
-
const handleSelectVariant = useCallback(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
requestAnimationFrame(() => {
|
|
244
|
-
const previewCanvas = document.getElementById("preview-canvas");
|
|
245
|
-
if (previewCanvas instanceof HTMLElement) {
|
|
246
|
-
previewCanvas.scrollTo({ top: 0, behavior: "smooth" });
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
}, [setUrlVariant]);
|
|
243
|
+
const handleSelectVariant = useCallback(
|
|
244
|
+
(index: number) => {
|
|
245
|
+
if (variantCount === 0) return;
|
|
246
|
+
const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
|
|
247
|
+
const variantName = variants[normalizedIndex]?.name;
|
|
248
|
+
setActiveVariantIndex(normalizedIndex);
|
|
249
|
+
setUrlVariant(variantName || null);
|
|
250
|
+
},
|
|
251
|
+
[variantCount, variants, setUrlVariant]
|
|
252
|
+
);
|
|
250
253
|
|
|
251
|
-
const handleSelectVariantLink = useCallback(
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}, [handleSelectVariant, isAllVariantsMode]);
|
|
254
|
+
const handleSelectVariantLink = useCallback(
|
|
255
|
+
(index: number) => {
|
|
256
|
+
// Always scroll to the variant section in the docs-like view
|
|
257
|
+
focusVariantInAllMode(index, true);
|
|
258
|
+
},
|
|
259
|
+
[focusVariantInAllMode]
|
|
260
|
+
);
|
|
259
261
|
|
|
260
262
|
// Copy link handler
|
|
261
263
|
const handleCopyLink = useCallback(async () => {
|
|
262
264
|
const copied = await copyUrl();
|
|
263
265
|
if (copied) {
|
|
264
266
|
uiActions.setLinkCopied(true);
|
|
265
|
-
success(
|
|
267
|
+
success("Copied", "Link copied to clipboard");
|
|
266
268
|
setTimeout(() => uiActions.setLinkCopied(false), 2000);
|
|
267
269
|
}
|
|
268
270
|
}, [copyUrl, success, uiActions]);
|
|
@@ -270,49 +272,39 @@ export function App({ fragments }: AppProps) {
|
|
|
270
272
|
// Sorted fragment paths for keyboard navigation
|
|
271
273
|
const sortedFragmentPaths = useMemo(() => {
|
|
272
274
|
return [...fragments]
|
|
273
|
-
.filter(s => s.fragment?.meta?.name)
|
|
275
|
+
.filter((s) => s.fragment?.meta?.name)
|
|
274
276
|
.sort((a, b) => a.fragment.meta.name.localeCompare(b.fragment.meta.name))
|
|
275
|
-
.map(s => s.path);
|
|
277
|
+
.map((s) => s.path);
|
|
276
278
|
}, [fragments]);
|
|
277
279
|
|
|
278
|
-
const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath ||
|
|
280
|
+
const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || "");
|
|
279
281
|
|
|
280
282
|
// Keyboard shortcuts
|
|
281
283
|
useKeyboardShortcuts(
|
|
282
284
|
{
|
|
283
285
|
nextComponent: () => {
|
|
284
|
-
const nextIndex =
|
|
286
|
+
const nextIndex =
|
|
287
|
+
currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
|
|
285
288
|
handleSelectFragment(sortedFragmentPaths[nextIndex]);
|
|
286
289
|
},
|
|
287
290
|
prevComponent: () => {
|
|
288
|
-
const prevIndex =
|
|
291
|
+
const prevIndex =
|
|
292
|
+
currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
|
|
289
293
|
handleSelectFragment(sortedFragmentPaths[prevIndex]);
|
|
290
294
|
},
|
|
291
295
|
nextVariant: () => {
|
|
292
296
|
if (variantCount === 0) return;
|
|
293
297
|
const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
|
|
294
|
-
|
|
295
|
-
focusVariantInAllMode(nextIndex, true);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
handleSelectVariant(nextIndex);
|
|
298
|
+
focusVariantInAllMode(nextIndex, true);
|
|
299
299
|
},
|
|
300
300
|
prevVariant: () => {
|
|
301
301
|
if (variantCount === 0) return;
|
|
302
302
|
const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
|
|
303
|
-
|
|
304
|
-
focusVariantInAllMode(prevIndex, true);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
handleSelectVariant(prevIndex);
|
|
303
|
+
focusVariantInAllMode(prevIndex, true);
|
|
308
304
|
},
|
|
309
305
|
goToVariant: (index) => {
|
|
310
306
|
if (index >= variantCount) return;
|
|
311
|
-
|
|
312
|
-
focusVariantInAllMode(index, true);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
handleSelectVariant(index);
|
|
307
|
+
focusVariantInAllMode(index, true);
|
|
316
308
|
},
|
|
317
309
|
toggleTheme: viewSettings.toggleTheme,
|
|
318
310
|
togglePanel: uiActions.togglePanel,
|
|
@@ -324,7 +316,7 @@ export function App({ fragments }: AppProps) {
|
|
|
324
316
|
escape: () => {
|
|
325
317
|
if (document.activeElement === searchInputRef.current) {
|
|
326
318
|
if (searchQuery) {
|
|
327
|
-
setSearchQuery(
|
|
319
|
+
setSearchQuery("");
|
|
328
320
|
} else {
|
|
329
321
|
searchInputRef.current.blur();
|
|
330
322
|
}
|
|
@@ -344,9 +336,28 @@ export function App({ fragments }: AppProps) {
|
|
|
344
336
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
345
337
|
<FragmentRenderer variant={variant}>
|
|
346
338
|
{(content, isLoading, error) => {
|
|
347
|
-
if (isLoading)
|
|
348
|
-
|
|
349
|
-
|
|
339
|
+
if (isLoading)
|
|
340
|
+
return (
|
|
341
|
+
<Stack align="center" justify="center" style={{ padding: "32px" }}>
|
|
342
|
+
<LoaderIndicator />
|
|
343
|
+
</Stack>
|
|
344
|
+
);
|
|
345
|
+
if (error)
|
|
346
|
+
return (
|
|
347
|
+
<EmptyVariantMessage
|
|
348
|
+
reason={`Error: ${error.message}`}
|
|
349
|
+
variantName={variant.name}
|
|
350
|
+
hint="Check the console for the full error stack trace."
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
if (content === null || content === undefined)
|
|
354
|
+
return (
|
|
355
|
+
<EmptyVariantMessage
|
|
356
|
+
reason="render() returned null or undefined"
|
|
357
|
+
variantName={variant.name}
|
|
358
|
+
hint="The variant's render function didn't return any JSX."
|
|
359
|
+
/>
|
|
360
|
+
);
|
|
350
361
|
return content;
|
|
351
362
|
}}
|
|
352
363
|
</FragmentRenderer>
|
|
@@ -367,7 +378,10 @@ export function App({ fragments }: AppProps) {
|
|
|
367
378
|
return (
|
|
368
379
|
<>
|
|
369
380
|
<ViewerStateSync fragments={fragments} activeVariantIndex={safeVariantIndex} />
|
|
370
|
-
<KeyboardShortcutsHelp
|
|
381
|
+
<KeyboardShortcutsHelp
|
|
382
|
+
isOpen={uiState.showShortcutsHelp}
|
|
383
|
+
onClose={() => uiActions.setShortcutsHelp(false)}
|
|
384
|
+
/>
|
|
371
385
|
<CommandPalette
|
|
372
386
|
isOpen={uiState.showCommandPalette}
|
|
373
387
|
onClose={() => uiActions.setCommandPalette(false)}
|
|
@@ -415,14 +429,12 @@ export function App({ fragments }: AppProps) {
|
|
|
415
429
|
/>
|
|
416
430
|
}
|
|
417
431
|
aside={
|
|
418
|
-
activeFragment && !uiState.showHealthDashboard ? (
|
|
432
|
+
uiState.showAside && activeFragment && !uiState.showHealthDashboard ? (
|
|
419
433
|
<PreviewAside
|
|
420
434
|
fragment={activeFragment.fragment}
|
|
421
435
|
variants={variants}
|
|
422
436
|
focusedVariantIndex={safeVariantIndex}
|
|
423
|
-
isAllVariantsMode={isAllVariantsMode}
|
|
424
437
|
activePanel={uiState.activePanel}
|
|
425
|
-
onSelectAllVariants={handleSelectAllVariants}
|
|
426
438
|
onSelectVariant={handleSelectVariantLink}
|
|
427
439
|
onCopyLink={handleCopyLink}
|
|
428
440
|
onShowShortcuts={uiActions.toggleShortcutsHelp}
|
|
@@ -431,12 +443,12 @@ export function App({ fragments }: AppProps) {
|
|
|
431
443
|
}
|
|
432
444
|
>
|
|
433
445
|
{uiState.showHealthDashboard ? (
|
|
434
|
-
<
|
|
435
|
-
<Box padding="lg" style={{ maxWidth:
|
|
446
|
+
<Box height="100%" overflow="auto" background="primary">
|
|
447
|
+
<Box padding="lg" style={{ maxWidth: "896px", margin: "0 auto" }}>
|
|
436
448
|
<HealthDashboard
|
|
437
449
|
fragments={fragments}
|
|
438
450
|
onNavigate={(componentName) => {
|
|
439
|
-
const target = fragments.find(s => s.fragment.meta.name === componentName);
|
|
451
|
+
const target = fragments.find((s) => s.fragment.meta.name === componentName);
|
|
440
452
|
if (target) {
|
|
441
453
|
uiActions.setHealthDashboard(false);
|
|
442
454
|
handleSelectFragment(target.path);
|
|
@@ -444,40 +456,39 @@ export function App({ fragments }: AppProps) {
|
|
|
444
456
|
}}
|
|
445
457
|
/>
|
|
446
458
|
</Box>
|
|
447
|
-
</
|
|
459
|
+
</Box>
|
|
448
460
|
) : activeFragment ? (
|
|
449
|
-
<
|
|
461
|
+
<Stack id="preview-layout" style={{ height: "100%" }}>
|
|
450
462
|
{/* Main Content Area */}
|
|
451
|
-
<
|
|
463
|
+
<Stack style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
|
|
452
464
|
{/* Preview Area */}
|
|
453
|
-
<
|
|
454
|
-
id="preview-canvas"
|
|
455
|
-
style={{
|
|
456
|
-
flex: 1,
|
|
457
|
-
overflow: 'auto',
|
|
458
|
-
position: 'relative',
|
|
459
|
-
}}
|
|
460
|
-
>
|
|
465
|
+
<Box id="preview-canvas" overflow="auto" style={{ flex: 1, position: "relative" }}>
|
|
461
466
|
{variantCount === 0 ? (
|
|
462
467
|
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
463
|
-
) :
|
|
464
|
-
<
|
|
468
|
+
) : uiState.showMatrixView ? (
|
|
469
|
+
<PreviewArea
|
|
465
470
|
componentName={activeFragment.fragment.meta.name}
|
|
466
471
|
fragmentPath={activeFragment.path}
|
|
472
|
+
variant={activeVariant}
|
|
467
473
|
variants={variants}
|
|
468
|
-
focusedVariantIndex={safeVariantIndex}
|
|
469
474
|
zoom={viewSettings.zoom}
|
|
470
475
|
viewport={viewSettings.viewport}
|
|
471
476
|
customSize={viewSettings.customSize}
|
|
472
477
|
previewTheme={resolvedTheme}
|
|
478
|
+
showMatrixView={true}
|
|
479
|
+
showMultiViewport={false}
|
|
473
480
|
showComparison={uiState.showComparison}
|
|
481
|
+
figmaUrl={figmaUrl}
|
|
474
482
|
allFigmaUrls={allFigmaUrls}
|
|
475
|
-
|
|
483
|
+
onSelectVariant={(index) => {
|
|
484
|
+
uiActions.setMatrixView(false);
|
|
485
|
+
handleSelectVariant(index);
|
|
486
|
+
}}
|
|
476
487
|
onRetry={uiActions.incrementPreviewKey}
|
|
477
|
-
|
|
478
|
-
|
|
488
|
+
renderContent={() => renderVariantWithProps(activeVariant)}
|
|
489
|
+
previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
|
|
479
490
|
/>
|
|
480
|
-
) : (
|
|
491
|
+
) : uiState.showMultiViewport ? (
|
|
481
492
|
<PreviewArea
|
|
482
493
|
componentName={activeFragment.fragment.meta.name}
|
|
483
494
|
fragmentPath={activeFragment.path}
|
|
@@ -487,55 +498,79 @@ export function App({ fragments }: AppProps) {
|
|
|
487
498
|
viewport={viewSettings.viewport}
|
|
488
499
|
customSize={viewSettings.customSize}
|
|
489
500
|
previewTheme={resolvedTheme}
|
|
490
|
-
showMatrixView={
|
|
491
|
-
showMultiViewport={
|
|
501
|
+
showMatrixView={false}
|
|
502
|
+
showMultiViewport={true}
|
|
492
503
|
showComparison={uiState.showComparison}
|
|
493
504
|
figmaUrl={figmaUrl}
|
|
494
505
|
allFigmaUrls={allFigmaUrls}
|
|
495
506
|
onSelectVariant={(index) => {
|
|
496
|
-
uiActions.setMatrixView(false);
|
|
497
507
|
handleSelectVariant(index);
|
|
498
508
|
}}
|
|
499
509
|
onRetry={uiActions.incrementPreviewKey}
|
|
500
510
|
renderContent={() => renderVariantWithProps(activeVariant)}
|
|
501
511
|
previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
|
|
502
512
|
/>
|
|
513
|
+
) : (
|
|
514
|
+
<Box
|
|
515
|
+
style={{
|
|
516
|
+
padding: "var(--fui-space-6) var(--fui-space-8)",
|
|
517
|
+
}}
|
|
518
|
+
>
|
|
519
|
+
<ComponentDocView
|
|
520
|
+
fragment={activeFragment}
|
|
521
|
+
fragments={fragments}
|
|
522
|
+
renderVariantContent={renderVariantWithProps}
|
|
523
|
+
onNavigateToComponent={(name) => {
|
|
524
|
+
const target = fragments.find((s) => s.fragment.meta.name === name);
|
|
525
|
+
if (target) handleSelectFragment(target.path);
|
|
526
|
+
}}
|
|
527
|
+
zoom={viewSettings.zoom}
|
|
528
|
+
viewport={viewSettings.viewport}
|
|
529
|
+
customSize={viewSettings.customSize}
|
|
530
|
+
previewTheme={resolvedTheme}
|
|
531
|
+
showComparison={uiState.showComparison}
|
|
532
|
+
allFigmaUrls={allFigmaUrls}
|
|
533
|
+
onRetry={uiActions.incrementPreviewKey}
|
|
534
|
+
previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
|
|
535
|
+
/>
|
|
536
|
+
</Box>
|
|
503
537
|
)}
|
|
504
|
-
</
|
|
505
|
-
</
|
|
538
|
+
</Box>
|
|
539
|
+
</Stack>
|
|
506
540
|
|
|
507
|
-
{
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
541
|
+
{activeVariant && (
|
|
542
|
+
<BottomPanel
|
|
543
|
+
fragment={activeFragment.fragment}
|
|
544
|
+
variant={activeVariant}
|
|
545
|
+
fragments={fragments}
|
|
546
|
+
open={uiState.panelOpen}
|
|
547
|
+
onOpenChange={uiActions.setPanelOpen}
|
|
548
|
+
activePanel={uiState.activePanel}
|
|
549
|
+
onPanelChange={uiActions.setActivePanel}
|
|
550
|
+
figmaUrl={figmaUrl}
|
|
551
|
+
figmaStyles={
|
|
552
|
+
figmaIntegration.figmaStyles.status === "success"
|
|
553
|
+
? figmaIntegration.figmaStyles.styles || null
|
|
554
|
+
: null
|
|
555
|
+
}
|
|
556
|
+
renderedStyles={figmaIntegration.renderedStyles}
|
|
557
|
+
figmaLoading={figmaIntegration.isLoading}
|
|
558
|
+
figmaError={figmaIntegration.errorMessage}
|
|
559
|
+
onFetchFigma={figmaIntegration.fetchFigmaStyles}
|
|
560
|
+
onRefreshRendered={figmaIntegration.extractRenderedStyles}
|
|
561
|
+
onNavigateToComponent={(name) => {
|
|
562
|
+
const target = fragments.find((s) => s.fragment.meta.name === name);
|
|
563
|
+
if (target) handleSelectFragment(target.path);
|
|
564
|
+
}}
|
|
565
|
+
previewKey={uiState.previewKey}
|
|
566
|
+
fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
569
|
+
</Stack>
|
|
535
570
|
) : (
|
|
536
|
-
<EmptyState style={{ height:
|
|
571
|
+
<EmptyState style={{ height: "100%" }}>
|
|
537
572
|
<EmptyState.Icon>
|
|
538
|
-
<EmptyIcon style={{ width:
|
|
573
|
+
<EmptyIcon style={{ width: "48px", height: "48px" }} />
|
|
539
574
|
</EmptyState.Icon>
|
|
540
575
|
<EmptyState.Title>No component selected</EmptyState.Title>
|
|
541
576
|
<EmptyState.Description>Select a component from the sidebar</EmptyState.Description>
|
|
@@ -545,591 +580,3 @@ export function App({ fragments }: AppProps) {
|
|
|
545
580
|
</>
|
|
546
581
|
);
|
|
547
582
|
}
|
|
548
|
-
|
|
549
|
-
// Top Toolbar Component
|
|
550
|
-
interface TopToolbarProps {
|
|
551
|
-
fragment: { path: string; fragment: FragmentDefinition };
|
|
552
|
-
viewSettings: ReturnType<typeof useViewSettings>;
|
|
553
|
-
uiState: ReturnType<typeof useAppState>['state'];
|
|
554
|
-
uiActions: ReturnType<typeof useAppState>['actions'];
|
|
555
|
-
figmaUrl?: string;
|
|
556
|
-
searchQuery: string;
|
|
557
|
-
onSearchChange: (value: string) => void;
|
|
558
|
-
searchInputRef: RefObject<HTMLInputElement>;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
interface ViewerHeaderProps {
|
|
562
|
-
showHealth: boolean;
|
|
563
|
-
searchQuery: string;
|
|
564
|
-
onSearchChange: (value: string) => void;
|
|
565
|
-
searchInputRef: RefObject<HTMLInputElement>;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
interface HeaderSearchProps {
|
|
569
|
-
value: string;
|
|
570
|
-
onChange: (value: string) => void;
|
|
571
|
-
inputRef: RefObject<HTMLInputElement>;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
interface PreviewAsideProps {
|
|
575
|
-
fragment: FragmentDefinition;
|
|
576
|
-
variants: FragmentVariant[];
|
|
577
|
-
focusedVariantIndex: number;
|
|
578
|
-
isAllVariantsMode: boolean;
|
|
579
|
-
activePanel: string;
|
|
580
|
-
onSelectAllVariants: () => void;
|
|
581
|
-
onSelectVariant: (index: number) => void;
|
|
582
|
-
onCopyLink: () => void;
|
|
583
|
-
onShowShortcuts: () => void;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
587
|
-
return (
|
|
588
|
-
<Header.Search expandable>
|
|
589
|
-
<Input
|
|
590
|
-
ref={inputRef}
|
|
591
|
-
value={value}
|
|
592
|
-
onChange={onChange}
|
|
593
|
-
placeholder="Search components"
|
|
594
|
-
aria-label="Search components"
|
|
595
|
-
size="sm"
|
|
596
|
-
style={{ width: '240px' }}
|
|
597
|
-
/>
|
|
598
|
-
</Header.Search>
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef }: ViewerHeaderProps) {
|
|
603
|
-
const { setTheme, resolvedTheme } = useTheme();
|
|
604
|
-
return (
|
|
605
|
-
<Header aria-label="Fragments viewer header">
|
|
606
|
-
<Header.Trigger />
|
|
607
|
-
<Header.Brand>
|
|
608
|
-
<Stack direction="row" gap="sm" align="center">
|
|
609
|
-
<FragmentsLogo size={20} />
|
|
610
|
-
<Text weight="medium" size="sm">{BRAND.name}</Text>
|
|
611
|
-
<Text size="xs" color="tertiary">{showHealth ? 'health dashboard' : 'preview'}</Text>
|
|
612
|
-
</Stack>
|
|
613
|
-
</Header.Brand>
|
|
614
|
-
<HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
|
|
615
|
-
<Header.Spacer />
|
|
616
|
-
<Header.Actions>
|
|
617
|
-
<WebMCPStatusIndicator />
|
|
618
|
-
<ThemeToggle
|
|
619
|
-
size="sm"
|
|
620
|
-
value={resolvedTheme}
|
|
621
|
-
onValueChange={(value) => setTheme(value)}
|
|
622
|
-
aria-label={`Theme: ${resolvedTheme}`}
|
|
623
|
-
/>
|
|
624
|
-
<a
|
|
625
|
-
href="https://github.com/ConanMcN/fragments"
|
|
626
|
-
target="_blank"
|
|
627
|
-
rel="noopener noreferrer"
|
|
628
|
-
style={{
|
|
629
|
-
display: 'flex',
|
|
630
|
-
alignItems: 'center',
|
|
631
|
-
justifyContent: 'center',
|
|
632
|
-
width: '32px',
|
|
633
|
-
height: '32px',
|
|
634
|
-
borderRadius: 'var(--radius-md, 6px)',
|
|
635
|
-
color: 'var(--text-secondary)',
|
|
636
|
-
transition: 'background-color 150ms ease, color 150ms ease',
|
|
637
|
-
}}
|
|
638
|
-
aria-label="View on GitHub"
|
|
639
|
-
>
|
|
640
|
-
<GitHubIcon />
|
|
641
|
-
</a>
|
|
642
|
-
</Header.Actions>
|
|
643
|
-
</Header>
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function PreviewAside({
|
|
648
|
-
fragment,
|
|
649
|
-
variants,
|
|
650
|
-
focusedVariantIndex,
|
|
651
|
-
isAllVariantsMode,
|
|
652
|
-
activePanel,
|
|
653
|
-
onSelectAllVariants,
|
|
654
|
-
onSelectVariant,
|
|
655
|
-
onCopyLink,
|
|
656
|
-
onShowShortcuts,
|
|
657
|
-
}: PreviewAsideProps) {
|
|
658
|
-
const focusedVariant = variants[focusedVariantIndex] || null;
|
|
659
|
-
|
|
660
|
-
const baseLinkStyle: CSSProperties = {
|
|
661
|
-
color: 'var(--text-secondary)',
|
|
662
|
-
textDecoration: 'none',
|
|
663
|
-
fontSize: '13px',
|
|
664
|
-
borderRadius: '6px',
|
|
665
|
-
padding: '4px 8px',
|
|
666
|
-
display: 'block',
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const getLinkStyle = (isActive = false): CSSProperties => (
|
|
670
|
-
isActive
|
|
671
|
-
? { ...baseLinkStyle, color: 'var(--text-primary)', backgroundColor: 'var(--bg-hover)' }
|
|
672
|
-
: baseLinkStyle
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
return (
|
|
676
|
-
<Box padding="md" style={{ position: 'sticky', top: '80px' }}>
|
|
677
|
-
<Stack gap="md">
|
|
678
|
-
<Stack gap="xs">
|
|
679
|
-
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em' }}>
|
|
680
|
-
On this page
|
|
681
|
-
</Text>
|
|
682
|
-
<a href="#preview-canvas" style={baseLinkStyle}>
|
|
683
|
-
Preview
|
|
684
|
-
</a>
|
|
685
|
-
<a href="#preview-tools" style={baseLinkStyle}>
|
|
686
|
-
Panels
|
|
687
|
-
</a>
|
|
688
|
-
{variants.length > 0 && (
|
|
689
|
-
<>
|
|
690
|
-
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '8px' }}>
|
|
691
|
-
Variants
|
|
692
|
-
</Text>
|
|
693
|
-
<a
|
|
694
|
-
href="#preview-canvas"
|
|
695
|
-
style={getLinkStyle(isAllVariantsMode)}
|
|
696
|
-
onClick={(event) => {
|
|
697
|
-
event.preventDefault();
|
|
698
|
-
onSelectAllVariants();
|
|
699
|
-
}}
|
|
700
|
-
>
|
|
701
|
-
All
|
|
702
|
-
</a>
|
|
703
|
-
{variants.map((variant, index) => {
|
|
704
|
-
const active = index === focusedVariantIndex;
|
|
705
|
-
const anchorId = getVariantSectionId(fragment.meta.name, variant.name);
|
|
706
|
-
|
|
707
|
-
return (
|
|
708
|
-
<a
|
|
709
|
-
key={variant.name}
|
|
710
|
-
href={`#${anchorId}`}
|
|
711
|
-
style={getLinkStyle(active)}
|
|
712
|
-
onClick={(event) => {
|
|
713
|
-
event.preventDefault();
|
|
714
|
-
onSelectVariant(index);
|
|
715
|
-
}}
|
|
716
|
-
>
|
|
717
|
-
{variant.name}
|
|
718
|
-
</a>
|
|
719
|
-
);
|
|
720
|
-
})}
|
|
721
|
-
</>
|
|
722
|
-
)}
|
|
723
|
-
</Stack>
|
|
724
|
-
<Separator />
|
|
725
|
-
<Stack gap="xs">
|
|
726
|
-
<Text size="sm" weight="medium">{fragment.meta.name}</Text>
|
|
727
|
-
<Text size="xs" color="secondary">
|
|
728
|
-
{isAllVariantsMode
|
|
729
|
-
? `Variant: All${focusedVariant ? ` (focused: ${focusedVariant.name})` : ''}`
|
|
730
|
-
: focusedVariant ? `Variant: ${focusedVariant.name}` : 'Select a variant'}
|
|
731
|
-
</Text>
|
|
732
|
-
<Text size="xs" color="tertiary">
|
|
733
|
-
Active panel: {activePanel}
|
|
734
|
-
</Text>
|
|
735
|
-
</Stack>
|
|
736
|
-
<Separator />
|
|
737
|
-
<Stack gap="xs">
|
|
738
|
-
<Button variant="ghost" size="sm" onClick={onCopyLink}>
|
|
739
|
-
Copy Link
|
|
740
|
-
</Button>
|
|
741
|
-
<Button variant="ghost" size="sm" onClick={onShowShortcuts}>
|
|
742
|
-
Keyboard Shortcuts
|
|
743
|
-
</Button>
|
|
744
|
-
</Stack>
|
|
745
|
-
</Stack>
|
|
746
|
-
</Box>
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function TopToolbar({
|
|
751
|
-
fragment,
|
|
752
|
-
viewSettings,
|
|
753
|
-
uiState,
|
|
754
|
-
uiActions,
|
|
755
|
-
figmaUrl,
|
|
756
|
-
searchQuery,
|
|
757
|
-
onSearchChange,
|
|
758
|
-
searchInputRef,
|
|
759
|
-
}: TopToolbarProps) {
|
|
760
|
-
const { setTheme, resolvedTheme } = useTheme();
|
|
761
|
-
return (
|
|
762
|
-
<Header aria-label="Component preview toolbar">
|
|
763
|
-
<Header.Trigger />
|
|
764
|
-
<Header.Brand>
|
|
765
|
-
<Stack direction="row" align="center" gap="sm">
|
|
766
|
-
<FragmentsLogo size={20} />
|
|
767
|
-
<Text weight="medium" size="sm">{fragment.fragment.meta.name}</Text>
|
|
768
|
-
<Text size="xs" color="tertiary">{fragment.fragment.meta.category}</Text>
|
|
769
|
-
</Stack>
|
|
770
|
-
</Header.Brand>
|
|
771
|
-
<HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
|
|
772
|
-
<Header.Spacer />
|
|
773
|
-
<Header.Actions>
|
|
774
|
-
<PreviewToolbar
|
|
775
|
-
zoom={viewSettings.zoom}
|
|
776
|
-
onZoomChange={viewSettings.setZoom}
|
|
777
|
-
/>
|
|
778
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
779
|
-
<Tooltip content={uiState.showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
|
|
780
|
-
<Button
|
|
781
|
-
variant="ghost"
|
|
782
|
-
size="sm"
|
|
783
|
-
aria-pressed={uiState.showMatrixView}
|
|
784
|
-
aria-label="Toggle matrix view"
|
|
785
|
-
onClick={() => uiActions.setMatrixView(!uiState.showMatrixView)}
|
|
786
|
-
style={uiState.showMatrixView ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
787
|
-
>
|
|
788
|
-
<GridFour size={16} />
|
|
789
|
-
</Button>
|
|
790
|
-
</Tooltip>
|
|
791
|
-
<Tooltip content={uiState.showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
|
|
792
|
-
<Button
|
|
793
|
-
variant="ghost"
|
|
794
|
-
size="sm"
|
|
795
|
-
aria-pressed={uiState.showMultiViewport}
|
|
796
|
-
aria-label="Toggle responsive view"
|
|
797
|
-
onClick={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
|
|
798
|
-
style={uiState.showMultiViewport ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
799
|
-
>
|
|
800
|
-
<DeviceMobile size={16} />
|
|
801
|
-
</Button>
|
|
802
|
-
</Tooltip>
|
|
803
|
-
<Tooltip content={uiState.panelOpen ? "Hide addons panel" : "Show addons panel"}>
|
|
804
|
-
<Button
|
|
805
|
-
variant="ghost"
|
|
806
|
-
size="sm"
|
|
807
|
-
aria-pressed={uiState.panelOpen}
|
|
808
|
-
aria-label="Toggle addons panel"
|
|
809
|
-
onClick={uiActions.togglePanel}
|
|
810
|
-
style={uiState.panelOpen ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
811
|
-
>
|
|
812
|
-
<Rows size={16} />
|
|
813
|
-
</Button>
|
|
814
|
-
</Tooltip>
|
|
815
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
816
|
-
{figmaUrl && (
|
|
817
|
-
<>
|
|
818
|
-
<Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
|
|
819
|
-
<Button
|
|
820
|
-
onClick={uiActions.toggleComparison}
|
|
821
|
-
variant="ghost"
|
|
822
|
-
size="sm"
|
|
823
|
-
style={uiState.showComparison ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
824
|
-
>
|
|
825
|
-
<CompareIcon style={{ width: '16px', height: '16px' }} />
|
|
826
|
-
</Button>
|
|
827
|
-
</Tooltip>
|
|
828
|
-
<Tooltip content="View in Figma">
|
|
829
|
-
<Button
|
|
830
|
-
onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
|
|
831
|
-
variant="ghost"
|
|
832
|
-
size="sm"
|
|
833
|
-
>
|
|
834
|
-
<FigmaIcon style={{ width: '16px', height: '16px' }} />
|
|
835
|
-
</Button>
|
|
836
|
-
</Tooltip>
|
|
837
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
838
|
-
</>
|
|
839
|
-
)}
|
|
840
|
-
<WebMCPStatusIndicator />
|
|
841
|
-
<ThemeToggle
|
|
842
|
-
size="sm"
|
|
843
|
-
value={resolvedTheme}
|
|
844
|
-
onValueChange={(value) => setTheme(value)}
|
|
845
|
-
aria-label={`Theme: ${resolvedTheme}`}
|
|
846
|
-
/>
|
|
847
|
-
<a
|
|
848
|
-
href="https://github.com/ConanMcN/fragments"
|
|
849
|
-
target="_blank"
|
|
850
|
-
rel="noopener noreferrer"
|
|
851
|
-
style={{
|
|
852
|
-
display: 'flex',
|
|
853
|
-
alignItems: 'center',
|
|
854
|
-
justifyContent: 'center',
|
|
855
|
-
width: '32px',
|
|
856
|
-
height: '32px',
|
|
857
|
-
borderRadius: 'var(--radius-md, 6px)',
|
|
858
|
-
color: 'var(--text-secondary)',
|
|
859
|
-
transition: 'background-color 150ms ease, color 150ms ease',
|
|
860
|
-
}}
|
|
861
|
-
aria-label="View on GitHub"
|
|
862
|
-
>
|
|
863
|
-
<GitHubIcon />
|
|
864
|
-
</a>
|
|
865
|
-
</Header.Actions>
|
|
866
|
-
</Header>
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function normalizeAnchorSegment(value: string): string {
|
|
871
|
-
const normalized = value
|
|
872
|
-
.toLowerCase()
|
|
873
|
-
.trim()
|
|
874
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
875
|
-
.replace(/^-+|-+$/g, "");
|
|
876
|
-
return normalized || "variant";
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function getVariantSectionId(componentName: string, variantName: string): string {
|
|
880
|
-
return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
interface AllVariantsPreviewProps {
|
|
884
|
-
componentName: string;
|
|
885
|
-
fragmentPath: string;
|
|
886
|
-
variants: FragmentVariant[];
|
|
887
|
-
focusedVariantIndex: number;
|
|
888
|
-
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
889
|
-
viewport: ReturnType<typeof useViewSettings>["viewport"];
|
|
890
|
-
customSize: ReturnType<typeof useViewSettings>["customSize"];
|
|
891
|
-
previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
|
|
892
|
-
showComparison: boolean;
|
|
893
|
-
allFigmaUrls: string[];
|
|
894
|
-
fallbackFigmaUrl?: string;
|
|
895
|
-
onRetry: () => void;
|
|
896
|
-
renderVariantContent: (variant: FragmentVariant) => ReactNode;
|
|
897
|
-
previewKeyBase: string;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function AllVariantsPreview({
|
|
901
|
-
componentName,
|
|
902
|
-
fragmentPath,
|
|
903
|
-
variants,
|
|
904
|
-
focusedVariantIndex,
|
|
905
|
-
zoom,
|
|
906
|
-
viewport,
|
|
907
|
-
customSize,
|
|
908
|
-
previewTheme,
|
|
909
|
-
showComparison,
|
|
910
|
-
allFigmaUrls,
|
|
911
|
-
fallbackFigmaUrl,
|
|
912
|
-
onRetry,
|
|
913
|
-
renderVariantContent,
|
|
914
|
-
previewKeyBase,
|
|
915
|
-
}: AllVariantsPreviewProps) {
|
|
916
|
-
return (
|
|
917
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
|
|
918
|
-
{variants.map((variant, index) => {
|
|
919
|
-
const isFocused = index === focusedVariantIndex;
|
|
920
|
-
|
|
921
|
-
return (
|
|
922
|
-
<section
|
|
923
|
-
id={getVariantSectionId(componentName, variant.name)}
|
|
924
|
-
key={variant.name}
|
|
925
|
-
style={{
|
|
926
|
-
border: '1px solid var(--border)',
|
|
927
|
-
borderColor: isFocused ? 'var(--color-accent)' : 'var(--border)',
|
|
928
|
-
borderRadius: '10px',
|
|
929
|
-
overflow: 'hidden',
|
|
930
|
-
backgroundColor: 'var(--bg-primary)',
|
|
931
|
-
boxShadow: isFocused ? '0 0 0 1px color-mix(in srgb, var(--color-accent) 40%, transparent)' : undefined,
|
|
932
|
-
}}
|
|
933
|
-
>
|
|
934
|
-
<Stack direction="row" align="baseline" gap="sm" style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
|
|
935
|
-
<Text size="sm" weight="medium">{variant.name}</Text>
|
|
936
|
-
<Text size="xs" color="secondary">{variant.description}</Text>
|
|
937
|
-
</Stack>
|
|
938
|
-
<PreviewArea
|
|
939
|
-
componentName={componentName}
|
|
940
|
-
fragmentPath={fragmentPath}
|
|
941
|
-
variant={variant}
|
|
942
|
-
variants={variants}
|
|
943
|
-
zoom={zoom}
|
|
944
|
-
viewport={viewport}
|
|
945
|
-
customSize={customSize}
|
|
946
|
-
previewTheme={previewTheme}
|
|
947
|
-
showMatrixView={false}
|
|
948
|
-
showMultiViewport={false}
|
|
949
|
-
showComparison={showComparison}
|
|
950
|
-
figmaUrl={variant.figma || fallbackFigmaUrl}
|
|
951
|
-
allFigmaUrls={allFigmaUrls}
|
|
952
|
-
onSelectVariant={() => {}}
|
|
953
|
-
onRetry={onRetry}
|
|
954
|
-
renderContent={() => renderVariantContent(variant)}
|
|
955
|
-
previewKey={`${previewKeyBase}-${index}`}
|
|
956
|
-
/>
|
|
957
|
-
</section>
|
|
958
|
-
);
|
|
959
|
-
})}
|
|
960
|
-
</div>
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// No variants message
|
|
965
|
-
interface NoVariantsMessageProps {
|
|
966
|
-
fragment?: FragmentDefinition;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
|
|
970
|
-
// Check for load error (missing dependencies, schema errors, etc.)
|
|
971
|
-
const loadError = (fragment as any)?._loadError;
|
|
972
|
-
if (loadError) {
|
|
973
|
-
return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const skippedVariants = (fragment?._generated as any)?.skippedVariants;
|
|
977
|
-
|
|
978
|
-
if (!skippedVariants || skippedVariants.length === 0) {
|
|
979
|
-
return (
|
|
980
|
-
<EmptyState style={{ height: '100%' }}>
|
|
981
|
-
<EmptyState.Description>No variants defined</EmptyState.Description>
|
|
982
|
-
</EmptyState>
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
return (
|
|
987
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
|
|
988
|
-
<Alert variant="info">
|
|
989
|
-
<Alert.Body>
|
|
990
|
-
<Alert.Title>
|
|
991
|
-
{skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
|
|
992
|
-
</Alert.Title>
|
|
993
|
-
<Alert.Content>
|
|
994
|
-
<Stack direction="column" gap="sm">
|
|
995
|
-
<Text size="xs" color="secondary">
|
|
996
|
-
These variants couldn't be rendered because they use syntax the parser doesn't support yet:
|
|
997
|
-
</Text>
|
|
998
|
-
<ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
|
|
999
|
-
{skippedVariants.map((sv: any, i: number) => (
|
|
1000
|
-
<li key={i}>
|
|
1001
|
-
<Text size="xs" color="secondary">
|
|
1002
|
-
<Text as="span" size="xs" weight="semibold">{sv.name}:</Text>{' '}
|
|
1003
|
-
<Text as="span" size="xs" color="tertiary">{sv.reason}</Text>
|
|
1004
|
-
</Text>
|
|
1005
|
-
</li>
|
|
1006
|
-
))}
|
|
1007
|
-
</ul>
|
|
1008
|
-
</Stack>
|
|
1009
|
-
</Alert.Content>
|
|
1010
|
-
</Alert.Body>
|
|
1011
|
-
</Alert>
|
|
1012
|
-
</div>
|
|
1013
|
-
);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Load error message — shown when a fragment failed to import (missing deps, schema errors, etc.)
|
|
1017
|
-
function LoadErrorMessage({ error, componentName }: { error: { message: string; dependencies: string[] }; componentName?: string }) {
|
|
1018
|
-
const deps = error.dependencies || [];
|
|
1019
|
-
const errorMessage = error.message || 'Unknown error';
|
|
1020
|
-
|
|
1021
|
-
// Determine if the error is a missing module/dependency issue
|
|
1022
|
-
const isModuleError = /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(errorMessage);
|
|
1023
|
-
|
|
1024
|
-
// Only suggest packages if the error is actually about missing modules
|
|
1025
|
-
let suggestedPackages: string[] = [];
|
|
1026
|
-
if (isModuleError) {
|
|
1027
|
-
if (deps.length > 0) {
|
|
1028
|
-
suggestedPackages = [...deps];
|
|
1029
|
-
} else {
|
|
1030
|
-
const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
|
|
1031
|
-
if (match) {
|
|
1032
|
-
suggestedPackages = [match[1]];
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const hasMissingDeps = suggestedPackages.length > 0;
|
|
1038
|
-
const installCmd = `pnpm add ${suggestedPackages.join(' ')}`;
|
|
1039
|
-
|
|
1040
|
-
return (
|
|
1041
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
|
|
1042
|
-
<Alert variant="warning">
|
|
1043
|
-
<Alert.Body>
|
|
1044
|
-
<Alert.Title>
|
|
1045
|
-
{hasMissingDeps ? 'Missing Dependencies' : 'Failed to Load'}
|
|
1046
|
-
</Alert.Title>
|
|
1047
|
-
<Alert.Content>
|
|
1048
|
-
<Stack direction="column" gap="sm">
|
|
1049
|
-
{hasMissingDeps ? (
|
|
1050
|
-
<>
|
|
1051
|
-
<Text size="xs" color="secondary">
|
|
1052
|
-
{componentName ? `${componentName} requires` : 'This component requires'} packages that are not installed in your project.
|
|
1053
|
-
</Text>
|
|
1054
|
-
<Text size="xs" weight="semibold" color="secondary">Install with:</Text>
|
|
1055
|
-
<code style={{
|
|
1056
|
-
display: 'block',
|
|
1057
|
-
padding: '8px 12px',
|
|
1058
|
-
borderRadius: '6px',
|
|
1059
|
-
backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
|
|
1060
|
-
fontFamily: 'monospace',
|
|
1061
|
-
fontSize: '12px',
|
|
1062
|
-
color: 'var(--text-primary, #111827)',
|
|
1063
|
-
userSelect: 'all',
|
|
1064
|
-
}}>
|
|
1065
|
-
{installCmd}
|
|
1066
|
-
</code>
|
|
1067
|
-
<Text size="xs" color="tertiary">
|
|
1068
|
-
After installing, restart the dev server.
|
|
1069
|
-
</Text>
|
|
1070
|
-
</>
|
|
1071
|
-
) : (
|
|
1072
|
-
<>
|
|
1073
|
-
<Text size="xs" color="secondary">
|
|
1074
|
-
{componentName ? `${componentName} couldn't` : 'This component couldn\'t'} be loaded. This may be due to a schema validation error or missing imports.
|
|
1075
|
-
</Text>
|
|
1076
|
-
<Text size="xs" weight="semibold" color="secondary">Error:</Text>
|
|
1077
|
-
<pre style={{
|
|
1078
|
-
padding: '8px 12px',
|
|
1079
|
-
borderRadius: '6px',
|
|
1080
|
-
backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
|
|
1081
|
-
fontFamily: 'monospace',
|
|
1082
|
-
fontSize: '11px',
|
|
1083
|
-
color: 'var(--text-secondary, #374151)',
|
|
1084
|
-
whiteSpace: 'pre-wrap',
|
|
1085
|
-
wordBreak: 'break-word',
|
|
1086
|
-
margin: 0,
|
|
1087
|
-
maxHeight: '200px',
|
|
1088
|
-
overflow: 'auto',
|
|
1089
|
-
}}>
|
|
1090
|
-
{errorMessage}
|
|
1091
|
-
</pre>
|
|
1092
|
-
</>
|
|
1093
|
-
)}
|
|
1094
|
-
</Stack>
|
|
1095
|
-
</Alert.Content>
|
|
1096
|
-
</Alert.Body>
|
|
1097
|
-
</Alert>
|
|
1098
|
-
</div>
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Empty variant message
|
|
1103
|
-
interface EmptyVariantMessageProps {
|
|
1104
|
-
reason: string;
|
|
1105
|
-
variantName: string;
|
|
1106
|
-
hint?: string;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
|
|
1110
|
-
return (
|
|
1111
|
-
<Alert variant="warning">
|
|
1112
|
-
<Alert.Body>
|
|
1113
|
-
<Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
|
|
1114
|
-
<Alert.Content>
|
|
1115
|
-
<Stack direction="column" gap="sm">
|
|
1116
|
-
<Text size="xs" color="secondary">{reason}</Text>
|
|
1117
|
-
{hint && (
|
|
1118
|
-
<Text size="xs" color="tertiary">
|
|
1119
|
-
<Text as="span" size="xs" weight="semibold">Tip:</Text> {hint}
|
|
1120
|
-
</Text>
|
|
1121
|
-
)}
|
|
1122
|
-
<div>
|
|
1123
|
-
<Text size="xs" color="tertiary" weight="semibold">Common causes:</Text>
|
|
1124
|
-
<ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
|
|
1125
|
-
<li><Text size="xs" color="secondary">Component requires props that weren't provided</Text></li>
|
|
1126
|
-
<li><Text size="xs" color="secondary">Component renders conditionally and conditions aren't met</Text></li>
|
|
1127
|
-
<li><Text size="xs" color="secondary">Story args reference variables that don't exist in this context</Text></li>
|
|
1128
|
-
</ul>
|
|
1129
|
-
</div>
|
|
1130
|
-
</Stack>
|
|
1131
|
-
</Alert.Content>
|
|
1132
|
-
</Alert.Body>
|
|
1133
|
-
</Alert>
|
|
1134
|
-
);
|
|
1135
|
-
}
|