@fragments-sdk/cli 0.7.10 → 0.7.12
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 +1 -1
- package/dist/{viewer-ZA7WK3EY.js → viewer-CNLZQUFO.js} +21 -4
- package/dist/viewer-CNLZQUFO.js.map +1 -0
- package/package.json +3 -2
- package/src/viewer/components/App.tsx +477 -126
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -1
- package/src/viewer/components/CodePanel.tsx +0 -1
- package/src/viewer/components/CommandPalette.tsx +17 -1
- package/src/viewer/components/IsolatedPreviewFrame.tsx +71 -74
- package/src/viewer/components/Layout.tsx +16 -13
- package/src/viewer/components/LeftSidebar.tsx +23 -3
- package/src/viewer/components/PreviewArea.tsx +21 -11
- package/src/viewer/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/viewer/server.ts +27 -3
- 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/dist/viewer-ZA7WK3EY.js.map +0 -1
- package/src/viewer/components/PreviewMenu.tsx +0 -247
|
@@ -3,19 +3,18 @@
|
|
|
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 {
|
|
17
|
+
import { PreviewToolbar } from "./PreviewToolbar.js";
|
|
19
18
|
import { getBackgroundStyle } from "../constants/ui.js";
|
|
20
19
|
|
|
21
20
|
// Preview & Rendering
|
|
@@ -28,7 +27,8 @@ 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
34
|
import { EmptyIcon, FigmaIcon, CompareIcon } from "./Icons.js";
|
|
@@ -99,6 +99,8 @@ export function App({ fragments }: AppProps) {
|
|
|
99
99
|
}
|
|
100
100
|
return fragments[0]?.path ?? null;
|
|
101
101
|
});
|
|
102
|
+
const activeFragmentPathRef = useRef(activeFragmentPath);
|
|
103
|
+
activeFragmentPathRef.current = activeFragmentPath;
|
|
102
104
|
|
|
103
105
|
const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
|
|
104
106
|
const fragment = fragments.find(s => s.path === activeFragmentPath);
|
|
@@ -115,7 +117,11 @@ export function App({ fragments }: AppProps) {
|
|
|
115
117
|
() => fragments.find((s) => s.path === activeFragmentPath),
|
|
116
118
|
[fragments, activeFragmentPath]
|
|
117
119
|
);
|
|
118
|
-
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;
|
|
119
125
|
const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
|
|
120
126
|
|
|
121
127
|
// Figma integration
|
|
@@ -146,17 +152,34 @@ export function App({ fragments }: AppProps) {
|
|
|
146
152
|
}
|
|
147
153
|
}, [uiState.showComparison, activeVariant, figmaIntegration.extractRenderedStyles, uiState.previewKey]);
|
|
148
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
|
+
|
|
149
166
|
// Sync URL state on browser navigation
|
|
150
167
|
useEffect(() => {
|
|
151
168
|
if (urlState.component) {
|
|
152
169
|
const found = findFragmentByName(fragments, urlState.component);
|
|
153
|
-
if (found
|
|
154
|
-
|
|
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) {
|
|
155
178
|
const variantIndex = findVariantIndex(found.fragment.variants, urlState.variant);
|
|
156
179
|
setActiveVariantIndex(variantIndex);
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
|
-
}, [urlState.component, urlState.variant, fragments,
|
|
182
|
+
}, [urlState.component, urlState.variant, fragments, uiActions]);
|
|
160
183
|
|
|
161
184
|
// HMR toast notifications
|
|
162
185
|
useEffect(() => {
|
|
@@ -178,19 +201,64 @@ export function App({ fragments }: AppProps) {
|
|
|
178
201
|
const handleSelectFragment = useCallback((path: string) => {
|
|
179
202
|
const fragment = fragments.find((s) => s.path === path);
|
|
180
203
|
const componentName = fragment?.fragment.meta.name || path;
|
|
181
|
-
const firstVariant = fragment?.fragment.variants?.[0]?.name;
|
|
182
204
|
|
|
183
205
|
setActiveFragmentPath(path);
|
|
184
206
|
setActiveVariantIndex(0);
|
|
185
207
|
uiActions.setHealthDashboard(false);
|
|
186
|
-
setUrlComponent(componentName,
|
|
208
|
+
setUrlComponent(componentName, null);
|
|
187
209
|
}, [fragments, setUrlComponent, uiActions]);
|
|
188
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
|
+
|
|
189
236
|
const handleSelectVariant = useCallback((index: number) => {
|
|
190
|
-
|
|
191
|
-
|
|
237
|
+
if (variantCount === 0) return;
|
|
238
|
+
const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
|
|
239
|
+
const variantName = variants[normalizedIndex]?.name;
|
|
240
|
+
setActiveVariantIndex(normalizedIndex);
|
|
192
241
|
setUrlVariant(variantName || null);
|
|
193
|
-
}, [
|
|
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]);
|
|
194
262
|
|
|
195
263
|
// Copy link handler
|
|
196
264
|
const handleCopyLink = useCallback(async () => {
|
|
@@ -202,11 +270,6 @@ export function App({ fragments }: AppProps) {
|
|
|
202
270
|
}
|
|
203
271
|
}, [copyUrl, success, uiActions]);
|
|
204
272
|
|
|
205
|
-
const focusSearchInput = useCallback(() => {
|
|
206
|
-
searchInputRef.current?.focus();
|
|
207
|
-
searchInputRef.current?.select();
|
|
208
|
-
}, []);
|
|
209
|
-
|
|
210
273
|
// Sorted fragment paths for keyboard navigation
|
|
211
274
|
const sortedFragmentPaths = useMemo(() => {
|
|
212
275
|
return [...fragments]
|
|
@@ -216,7 +279,6 @@ export function App({ fragments }: AppProps) {
|
|
|
216
279
|
}, [fragments]);
|
|
217
280
|
|
|
218
281
|
const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || '');
|
|
219
|
-
const variantCount = activeFragment?.fragment.variants?.length || 0;
|
|
220
282
|
|
|
221
283
|
// Keyboard shortcuts
|
|
222
284
|
useKeyboardShortcuts(
|
|
@@ -229,16 +291,39 @@ export function App({ fragments }: AppProps) {
|
|
|
229
291
|
const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
|
|
230
292
|
handleSelectFragment(sortedFragmentPaths[prevIndex]);
|
|
231
293
|
},
|
|
232
|
-
nextVariant: () =>
|
|
233
|
-
|
|
234
|
-
|
|
294
|
+
nextVariant: () => {
|
|
295
|
+
if (variantCount === 0) return;
|
|
296
|
+
const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
|
|
297
|
+
if (isAllVariantsMode) {
|
|
298
|
+
focusVariantInAllMode(nextIndex, true);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
handleSelectVariant(nextIndex);
|
|
302
|
+
},
|
|
303
|
+
prevVariant: () => {
|
|
304
|
+
if (variantCount === 0) return;
|
|
305
|
+
const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
|
|
306
|
+
if (isAllVariantsMode) {
|
|
307
|
+
focusVariantInAllMode(prevIndex, true);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
handleSelectVariant(prevIndex);
|
|
311
|
+
},
|
|
312
|
+
goToVariant: (index) => {
|
|
313
|
+
if (index >= variantCount) return;
|
|
314
|
+
if (isAllVariantsMode) {
|
|
315
|
+
focusVariantInAllMode(index, true);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
handleSelectVariant(index);
|
|
319
|
+
},
|
|
235
320
|
toggleTheme: viewSettings.toggleTheme,
|
|
236
321
|
togglePanel: uiActions.togglePanel,
|
|
237
322
|
toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
|
|
238
323
|
toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
|
|
239
324
|
copyLink: handleCopyLink,
|
|
240
325
|
showHelp: uiActions.toggleShortcutsHelp,
|
|
241
|
-
openSearch:
|
|
326
|
+
openSearch: () => uiActions.setCommandPalette(true),
|
|
242
327
|
escape: () => {
|
|
243
328
|
if (document.activeElement === searchInputRef.current) {
|
|
244
329
|
if (searchQuery) {
|
|
@@ -255,22 +340,22 @@ export function App({ fragments }: AppProps) {
|
|
|
255
340
|
);
|
|
256
341
|
|
|
257
342
|
// Render variant with action logging via DOM event capture
|
|
258
|
-
const renderVariantWithProps = useCallback(() => {
|
|
259
|
-
if (!
|
|
343
|
+
const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
|
|
344
|
+
if (!variant) return null;
|
|
260
345
|
|
|
261
346
|
return (
|
|
262
347
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
263
|
-
<StoryRenderer variant={
|
|
348
|
+
<StoryRenderer variant={variant}>
|
|
264
349
|
{(content, isLoading, error) => {
|
|
265
350
|
if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
|
|
266
|
-
if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={
|
|
267
|
-
if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={
|
|
351
|
+
if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={variant.name} hint="Check the console for the full error stack trace." />;
|
|
352
|
+
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." />;
|
|
268
353
|
return content;
|
|
269
354
|
}}
|
|
270
355
|
</StoryRenderer>
|
|
271
356
|
</ActionCapture>
|
|
272
357
|
);
|
|
273
|
-
}, [
|
|
358
|
+
}, []);
|
|
274
359
|
|
|
275
360
|
// Check if isolated mode
|
|
276
361
|
const isIsolated = useMemo(() => {
|
|
@@ -301,24 +386,12 @@ export function App({ fragments }: AppProps) {
|
|
|
301
386
|
activeFragment && !uiState.showHealthDashboard ? (
|
|
302
387
|
<TopToolbar
|
|
303
388
|
fragment={activeFragment}
|
|
304
|
-
variant={activeVariant}
|
|
305
|
-
viewSettings={viewSettings}
|
|
306
389
|
uiState={uiState}
|
|
307
390
|
uiActions={uiActions}
|
|
308
391
|
figmaUrl={figmaUrl}
|
|
309
392
|
searchQuery={searchQuery}
|
|
310
393
|
onSearchChange={setSearchQuery}
|
|
311
394
|
searchInputRef={searchInputRef}
|
|
312
|
-
onPrevComponent={() => {
|
|
313
|
-
const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
|
|
314
|
-
handleSelectFragment(sortedFragmentPaths[prevIndex]);
|
|
315
|
-
}}
|
|
316
|
-
onNextComponent={() => {
|
|
317
|
-
const nextIndex = currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
|
|
318
|
-
handleSelectFragment(sortedFragmentPaths[nextIndex]);
|
|
319
|
-
}}
|
|
320
|
-
onPrevVariant={() => handleSelectVariant(activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1)}
|
|
321
|
-
onNextVariant={() => handleSelectVariant(activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0)}
|
|
322
395
|
/>
|
|
323
396
|
) : (
|
|
324
397
|
<ViewerHeader
|
|
@@ -342,6 +415,21 @@ export function App({ fragments }: AppProps) {
|
|
|
342
415
|
}}
|
|
343
416
|
/>
|
|
344
417
|
}
|
|
418
|
+
aside={
|
|
419
|
+
activeFragment && !uiState.showHealthDashboard ? (
|
|
420
|
+
<PreviewAside
|
|
421
|
+
fragment={activeFragment.fragment}
|
|
422
|
+
variants={variants}
|
|
423
|
+
focusedVariantIndex={safeVariantIndex}
|
|
424
|
+
isAllVariantsMode={isAllVariantsMode}
|
|
425
|
+
activePanel={uiState.activePanel}
|
|
426
|
+
onSelectAllVariants={handleSelectAllVariants}
|
|
427
|
+
onSelectVariant={handleSelectVariantLink}
|
|
428
|
+
onCopyLink={handleCopyLink}
|
|
429
|
+
onShowShortcuts={uiActions.toggleShortcutsHelp}
|
|
430
|
+
/>
|
|
431
|
+
) : null
|
|
432
|
+
}
|
|
345
433
|
>
|
|
346
434
|
{uiState.showHealthDashboard ? (
|
|
347
435
|
<div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
|
|
@@ -359,21 +447,25 @@ export function App({ fragments }: AppProps) {
|
|
|
359
447
|
</Box>
|
|
360
448
|
</div>
|
|
361
449
|
) : activeFragment ? (
|
|
362
|
-
<div style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
|
|
450
|
+
<div id="preview-layout" style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
|
|
363
451
|
{/* Main Content Area */}
|
|
364
452
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
453
|
+
<PreviewControlsBar
|
|
454
|
+
zoom={viewSettings.zoom}
|
|
455
|
+
background={viewSettings.background}
|
|
456
|
+
onZoomChange={viewSettings.setZoom}
|
|
457
|
+
onBackgroundChange={viewSettings.setBackground}
|
|
458
|
+
showMatrixView={uiState.showMatrixView}
|
|
459
|
+
showMultiViewport={uiState.showMultiViewport}
|
|
460
|
+
panelOpen={uiState.panelOpen}
|
|
461
|
+
onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
|
|
462
|
+
onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
|
|
463
|
+
onTogglePanel={uiActions.togglePanel}
|
|
464
|
+
/>
|
|
374
465
|
|
|
375
466
|
{/* Preview Area */}
|
|
376
467
|
<div
|
|
468
|
+
id="preview-canvas"
|
|
377
469
|
style={{
|
|
378
470
|
flex: 1,
|
|
379
471
|
overflow: 'auto',
|
|
@@ -381,12 +473,32 @@ export function App({ fragments }: AppProps) {
|
|
|
381
473
|
...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
|
|
382
474
|
}}
|
|
383
475
|
>
|
|
384
|
-
{
|
|
476
|
+
{variantCount === 0 ? (
|
|
477
|
+
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
478
|
+
) : isAllVariantsMode && !uiState.showMatrixView && !uiState.showMultiViewport ? (
|
|
479
|
+
<AllVariantsPreview
|
|
480
|
+
componentName={activeFragment.fragment.meta.name}
|
|
481
|
+
fragmentPath={activeFragment.path}
|
|
482
|
+
variants={variants}
|
|
483
|
+
focusedVariantIndex={safeVariantIndex}
|
|
484
|
+
zoom={viewSettings.zoom}
|
|
485
|
+
background={viewSettings.background}
|
|
486
|
+
viewport={viewSettings.viewport}
|
|
487
|
+
customSize={viewSettings.customSize}
|
|
488
|
+
previewTheme={resolvedTheme}
|
|
489
|
+
showComparison={uiState.showComparison}
|
|
490
|
+
allFigmaUrls={allFigmaUrls}
|
|
491
|
+
fallbackFigmaUrl={activeFragment.fragment.meta.figma}
|
|
492
|
+
onRetry={uiActions.incrementPreviewKey}
|
|
493
|
+
renderVariantContent={renderVariantWithProps}
|
|
494
|
+
previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
|
|
495
|
+
/>
|
|
496
|
+
) : (
|
|
385
497
|
<PreviewArea
|
|
386
498
|
componentName={activeFragment.fragment.meta.name}
|
|
387
499
|
fragmentPath={activeFragment.path}
|
|
388
500
|
variant={activeVariant}
|
|
389
|
-
variants={
|
|
501
|
+
variants={variants}
|
|
390
502
|
zoom={viewSettings.zoom}
|
|
391
503
|
background={viewSettings.background}
|
|
392
504
|
viewport={viewSettings.viewport}
|
|
@@ -402,40 +514,40 @@ export function App({ fragments }: AppProps) {
|
|
|
402
514
|
handleSelectVariant(index);
|
|
403
515
|
}}
|
|
404
516
|
onRetry={uiActions.incrementPreviewKey}
|
|
405
|
-
renderContent={renderVariantWithProps}
|
|
406
|
-
previewKey={`${activeFragmentPath}-${
|
|
517
|
+
renderContent={() => renderVariantWithProps(activeVariant)}
|
|
518
|
+
previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
|
|
407
519
|
/>
|
|
408
|
-
) : (
|
|
409
|
-
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
410
520
|
)}
|
|
411
521
|
</div>
|
|
412
522
|
</div>
|
|
413
523
|
|
|
414
524
|
{/* Bottom Panel */}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
525
|
+
<div id="preview-tools">
|
|
526
|
+
{uiState.panelOpen && activeVariant && (
|
|
527
|
+
<BottomPanel
|
|
528
|
+
fragment={activeFragment.fragment}
|
|
529
|
+
variant={activeVariant}
|
|
530
|
+
fragments={fragments}
|
|
531
|
+
activePanel={uiState.activePanel}
|
|
532
|
+
onPanelChange={uiActions.setActivePanel}
|
|
533
|
+
figmaUrl={figmaUrl}
|
|
534
|
+
figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
|
|
535
|
+
renderedStyles={figmaIntegration.renderedStyles}
|
|
536
|
+
figmaLoading={figmaIntegration.isLoading}
|
|
537
|
+
figmaError={figmaIntegration.errorMessage}
|
|
538
|
+
onFetchFigma={figmaIntegration.fetchFigmaStyles}
|
|
539
|
+
onRefreshRendered={figmaIntegration.extractRenderedStyles}
|
|
540
|
+
actionLogs={actionLogs}
|
|
541
|
+
onClearActionLogs={clearActionLogs}
|
|
542
|
+
onNavigateToComponent={(name) => {
|
|
543
|
+
const target = fragments.find(s => s.fragment.meta.name === name);
|
|
544
|
+
if (target) handleSelectFragment(target.path);
|
|
545
|
+
}}
|
|
546
|
+
previewKey={uiState.previewKey}
|
|
547
|
+
fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
|
|
548
|
+
/>
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
439
551
|
</div>
|
|
440
552
|
) : (
|
|
441
553
|
<EmptyState style={{ height: '100%' }}>
|
|
@@ -454,18 +566,12 @@ export function App({ fragments }: AppProps) {
|
|
|
454
566
|
// Top Toolbar Component
|
|
455
567
|
interface TopToolbarProps {
|
|
456
568
|
fragment: { path: string; fragment: FragmentDefinition };
|
|
457
|
-
variant: any;
|
|
458
|
-
viewSettings: ReturnType<typeof useViewSettings>;
|
|
459
569
|
uiState: ReturnType<typeof useAppState>['state'];
|
|
460
570
|
uiActions: ReturnType<typeof useAppState>['actions'];
|
|
461
571
|
figmaUrl?: string;
|
|
462
572
|
searchQuery: string;
|
|
463
573
|
onSearchChange: (value: string) => void;
|
|
464
574
|
searchInputRef: RefObject<HTMLInputElement>;
|
|
465
|
-
onPrevComponent: () => void;
|
|
466
|
-
onNextComponent: () => void;
|
|
467
|
-
onPrevVariant: () => void;
|
|
468
|
-
onNextVariant: () => void;
|
|
469
575
|
}
|
|
470
576
|
|
|
471
577
|
interface ViewerHeaderProps {
|
|
@@ -481,6 +587,18 @@ interface HeaderSearchProps {
|
|
|
481
587
|
inputRef: RefObject<HTMLInputElement>;
|
|
482
588
|
}
|
|
483
589
|
|
|
590
|
+
interface PreviewAsideProps {
|
|
591
|
+
fragment: FragmentDefinition;
|
|
592
|
+
variants: FragmentVariant[];
|
|
593
|
+
focusedVariantIndex: number;
|
|
594
|
+
isAllVariantsMode: boolean;
|
|
595
|
+
activePanel: string;
|
|
596
|
+
onSelectAllVariants: () => void;
|
|
597
|
+
onSelectVariant: (index: number) => void;
|
|
598
|
+
onCopyLink: () => void;
|
|
599
|
+
onShowShortcuts: () => void;
|
|
600
|
+
}
|
|
601
|
+
|
|
484
602
|
function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
485
603
|
return (
|
|
486
604
|
<Header.Search expandable>
|
|
@@ -491,7 +609,6 @@ function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
|
491
609
|
placeholder="Search components"
|
|
492
610
|
aria-label="Search components"
|
|
493
611
|
size="sm"
|
|
494
|
-
shortcut="⌘K"
|
|
495
612
|
style={{ width: '240px' }}
|
|
496
613
|
/>
|
|
497
614
|
</Header.Search>
|
|
@@ -542,43 +659,122 @@ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef
|
|
|
542
659
|
);
|
|
543
660
|
}
|
|
544
661
|
|
|
662
|
+
function PreviewAside({
|
|
663
|
+
fragment,
|
|
664
|
+
variants,
|
|
665
|
+
focusedVariantIndex,
|
|
666
|
+
isAllVariantsMode,
|
|
667
|
+
activePanel,
|
|
668
|
+
onSelectAllVariants,
|
|
669
|
+
onSelectVariant,
|
|
670
|
+
onCopyLink,
|
|
671
|
+
onShowShortcuts,
|
|
672
|
+
}: PreviewAsideProps) {
|
|
673
|
+
const focusedVariant = variants[focusedVariantIndex] || null;
|
|
674
|
+
|
|
675
|
+
const baseLinkStyle: CSSProperties = {
|
|
676
|
+
color: 'var(--text-secondary)',
|
|
677
|
+
textDecoration: 'none',
|
|
678
|
+
fontSize: '13px',
|
|
679
|
+
borderRadius: '6px',
|
|
680
|
+
padding: '4px 8px',
|
|
681
|
+
display: 'block',
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const getLinkStyle = (isActive = false): CSSProperties => (
|
|
685
|
+
isActive
|
|
686
|
+
? { ...baseLinkStyle, color: 'var(--text-primary)', backgroundColor: 'var(--bg-hover)' }
|
|
687
|
+
: baseLinkStyle
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
return (
|
|
691
|
+
<Box padding="md" style={{ position: 'sticky', top: '80px' }}>
|
|
692
|
+
<Stack gap="md">
|
|
693
|
+
<Stack gap="xs">
|
|
694
|
+
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em' }}>
|
|
695
|
+
On this page
|
|
696
|
+
</Text>
|
|
697
|
+
<a href="#preview-canvas" style={baseLinkStyle}>
|
|
698
|
+
Preview
|
|
699
|
+
</a>
|
|
700
|
+
<a href="#preview-tools" style={baseLinkStyle}>
|
|
701
|
+
Panels
|
|
702
|
+
</a>
|
|
703
|
+
{variants.length > 0 && (
|
|
704
|
+
<>
|
|
705
|
+
<Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '8px' }}>
|
|
706
|
+
Variants
|
|
707
|
+
</Text>
|
|
708
|
+
<a
|
|
709
|
+
href="#preview-canvas"
|
|
710
|
+
style={getLinkStyle(isAllVariantsMode)}
|
|
711
|
+
onClick={(event) => {
|
|
712
|
+
event.preventDefault();
|
|
713
|
+
onSelectAllVariants();
|
|
714
|
+
}}
|
|
715
|
+
>
|
|
716
|
+
All
|
|
717
|
+
</a>
|
|
718
|
+
{variants.map((variant, index) => {
|
|
719
|
+
const active = index === focusedVariantIndex;
|
|
720
|
+
const anchorId = getVariantSectionId(fragment.meta.name, variant.name);
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<a
|
|
724
|
+
key={variant.name}
|
|
725
|
+
href={`#${anchorId}`}
|
|
726
|
+
style={getLinkStyle(active)}
|
|
727
|
+
onClick={(event) => {
|
|
728
|
+
event.preventDefault();
|
|
729
|
+
onSelectVariant(index);
|
|
730
|
+
}}
|
|
731
|
+
>
|
|
732
|
+
{variant.name}
|
|
733
|
+
</a>
|
|
734
|
+
);
|
|
735
|
+
})}
|
|
736
|
+
</>
|
|
737
|
+
)}
|
|
738
|
+
</Stack>
|
|
739
|
+
<Separator />
|
|
740
|
+
<Stack gap="xs">
|
|
741
|
+
<Text size="sm" weight="medium">{fragment.meta.name}</Text>
|
|
742
|
+
<Text size="xs" color="secondary">
|
|
743
|
+
{isAllVariantsMode
|
|
744
|
+
? `Variant: All${focusedVariant ? ` (focused: ${focusedVariant.name})` : ''}`
|
|
745
|
+
: focusedVariant ? `Variant: ${focusedVariant.name}` : 'Select a variant'}
|
|
746
|
+
</Text>
|
|
747
|
+
<Text size="xs" color="tertiary">
|
|
748
|
+
Active panel: {activePanel}
|
|
749
|
+
</Text>
|
|
750
|
+
</Stack>
|
|
751
|
+
<Separator />
|
|
752
|
+
<Stack gap="xs">
|
|
753
|
+
<Button variant="ghost" size="sm" onClick={onCopyLink}>
|
|
754
|
+
Copy Link
|
|
755
|
+
</Button>
|
|
756
|
+
<Button variant="ghost" size="sm" onClick={onShowShortcuts}>
|
|
757
|
+
Keyboard Shortcuts
|
|
758
|
+
</Button>
|
|
759
|
+
</Stack>
|
|
760
|
+
</Stack>
|
|
761
|
+
</Box>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
545
765
|
function TopToolbar({
|
|
546
766
|
fragment,
|
|
547
|
-
variant,
|
|
548
|
-
viewSettings,
|
|
549
767
|
uiState,
|
|
550
768
|
uiActions,
|
|
551
769
|
figmaUrl,
|
|
552
770
|
searchQuery,
|
|
553
771
|
onSearchChange,
|
|
554
772
|
searchInputRef,
|
|
555
|
-
onPrevComponent,
|
|
556
|
-
onNextComponent,
|
|
557
|
-
onPrevVariant,
|
|
558
|
-
onNextVariant,
|
|
559
773
|
}: TopToolbarProps) {
|
|
560
774
|
const { setTheme, resolvedTheme } = useTheme();
|
|
561
775
|
return (
|
|
562
776
|
<Header aria-label="Component preview toolbar">
|
|
563
777
|
<Header.Trigger />
|
|
564
|
-
<PreviewMenu
|
|
565
|
-
zoom={viewSettings.zoom}
|
|
566
|
-
background={viewSettings.background}
|
|
567
|
-
viewport={viewSettings.viewport}
|
|
568
|
-
showMatrixView={uiState.showMatrixView}
|
|
569
|
-
showMultiViewport={uiState.showMultiViewport}
|
|
570
|
-
panelOpen={uiState.panelOpen}
|
|
571
|
-
onZoomChange={viewSettings.setZoom}
|
|
572
|
-
onBackgroundChange={viewSettings.setBackground}
|
|
573
|
-
onViewportChange={viewSettings.setViewport}
|
|
574
|
-
onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
|
|
575
|
-
onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
|
|
576
|
-
onTogglePanel={uiActions.togglePanel}
|
|
577
|
-
onPrevComponent={onPrevComponent}
|
|
578
|
-
onNextComponent={onNextComponent}
|
|
579
|
-
onPrevVariant={onPrevVariant}
|
|
580
|
-
onNextVariant={onNextVariant}
|
|
581
|
-
/>
|
|
582
778
|
<Header.Brand>
|
|
583
779
|
<Stack direction="row" align="center" gap="sm">
|
|
584
780
|
<img src={fragmentsLogo} alt="" width={20} height={20} style={{ display: 'block' }} />
|
|
@@ -642,24 +838,179 @@ function TopToolbar({
|
|
|
642
838
|
);
|
|
643
839
|
}
|
|
644
840
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
841
|
+
function normalizeAnchorSegment(value: string): string {
|
|
842
|
+
const normalized = value
|
|
843
|
+
.toLowerCase()
|
|
844
|
+
.trim()
|
|
845
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
846
|
+
.replace(/^-+|-+$/g, "");
|
|
847
|
+
return normalized || "variant";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function getVariantSectionId(componentName: string, variantName: string): string {
|
|
851
|
+
return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
interface PreviewControlsBarProps {
|
|
855
|
+
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
856
|
+
background: ReturnType<typeof useViewSettings>["background"];
|
|
857
|
+
onZoomChange: ReturnType<typeof useViewSettings>["setZoom"];
|
|
858
|
+
onBackgroundChange: ReturnType<typeof useViewSettings>["setBackground"];
|
|
650
859
|
showMatrixView: boolean;
|
|
860
|
+
showMultiViewport: boolean;
|
|
861
|
+
panelOpen: boolean;
|
|
862
|
+
onToggleMatrix: () => void;
|
|
863
|
+
onToggleMultiViewport: () => void;
|
|
864
|
+
onTogglePanel: () => void;
|
|
651
865
|
}
|
|
652
866
|
|
|
653
|
-
function
|
|
867
|
+
function PreviewControlsBar({
|
|
868
|
+
zoom,
|
|
869
|
+
background,
|
|
870
|
+
onZoomChange,
|
|
871
|
+
onBackgroundChange,
|
|
872
|
+
showMatrixView,
|
|
873
|
+
showMultiViewport,
|
|
874
|
+
panelOpen,
|
|
875
|
+
onToggleMatrix,
|
|
876
|
+
onToggleMultiViewport,
|
|
877
|
+
onTogglePanel,
|
|
878
|
+
}: PreviewControlsBarProps) {
|
|
879
|
+
const toggleButtonStyle = (active: boolean): CSSProperties => (
|
|
880
|
+
active ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}
|
|
881
|
+
);
|
|
882
|
+
|
|
654
883
|
return (
|
|
655
884
|
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
|
|
656
|
-
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
885
|
+
<Stack direction="row" gap="sm" align="center" justify="end">
|
|
886
|
+
<PreviewToolbar
|
|
887
|
+
zoom={zoom}
|
|
888
|
+
background={background}
|
|
889
|
+
onZoomChange={onZoomChange}
|
|
890
|
+
onBackgroundChange={onBackgroundChange}
|
|
891
|
+
/>
|
|
892
|
+
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
893
|
+
<Tooltip content={showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
|
|
894
|
+
<Button
|
|
895
|
+
variant="ghost"
|
|
896
|
+
size="sm"
|
|
897
|
+
aria-pressed={showMatrixView}
|
|
898
|
+
aria-label="Toggle matrix view"
|
|
899
|
+
onClick={onToggleMatrix}
|
|
900
|
+
style={toggleButtonStyle(showMatrixView)}
|
|
901
|
+
>
|
|
902
|
+
<GridFour size={16} />
|
|
903
|
+
</Button>
|
|
904
|
+
</Tooltip>
|
|
905
|
+
<Tooltip content={showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
|
|
906
|
+
<Button
|
|
907
|
+
variant="ghost"
|
|
908
|
+
size="sm"
|
|
909
|
+
aria-pressed={showMultiViewport}
|
|
910
|
+
aria-label="Toggle responsive view"
|
|
911
|
+
onClick={onToggleMultiViewport}
|
|
912
|
+
style={toggleButtonStyle(showMultiViewport)}
|
|
913
|
+
>
|
|
914
|
+
<DeviceMobile size={16} />
|
|
915
|
+
</Button>
|
|
916
|
+
</Tooltip>
|
|
917
|
+
<Tooltip content={panelOpen ? "Hide addons panel" : "Show addons panel"}>
|
|
918
|
+
<Button
|
|
919
|
+
variant="ghost"
|
|
920
|
+
size="sm"
|
|
921
|
+
aria-pressed={panelOpen}
|
|
922
|
+
aria-label="Toggle addons panel"
|
|
923
|
+
onClick={onTogglePanel}
|
|
924
|
+
style={toggleButtonStyle(panelOpen)}
|
|
925
|
+
>
|
|
926
|
+
<Rows size={16} />
|
|
927
|
+
</Button>
|
|
928
|
+
</Tooltip>
|
|
929
|
+
</Stack>
|
|
930
|
+
</div>
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
interface AllVariantsPreviewProps {
|
|
935
|
+
componentName: string;
|
|
936
|
+
fragmentPath: string;
|
|
937
|
+
variants: FragmentVariant[];
|
|
938
|
+
focusedVariantIndex: number;
|
|
939
|
+
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
940
|
+
background: ReturnType<typeof useViewSettings>["background"];
|
|
941
|
+
viewport: ReturnType<typeof useViewSettings>["viewport"];
|
|
942
|
+
customSize: ReturnType<typeof useViewSettings>["customSize"];
|
|
943
|
+
previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
|
|
944
|
+
showComparison: boolean;
|
|
945
|
+
allFigmaUrls: string[];
|
|
946
|
+
fallbackFigmaUrl?: string;
|
|
947
|
+
onRetry: () => void;
|
|
948
|
+
renderVariantContent: (variant: FragmentVariant) => ReactNode;
|
|
949
|
+
previewKeyBase: string;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function AllVariantsPreview({
|
|
953
|
+
componentName,
|
|
954
|
+
fragmentPath,
|
|
955
|
+
variants,
|
|
956
|
+
focusedVariantIndex,
|
|
957
|
+
zoom,
|
|
958
|
+
background,
|
|
959
|
+
viewport,
|
|
960
|
+
customSize,
|
|
961
|
+
previewTheme,
|
|
962
|
+
showComparison,
|
|
963
|
+
allFigmaUrls,
|
|
964
|
+
fallbackFigmaUrl,
|
|
965
|
+
onRetry,
|
|
966
|
+
renderVariantContent,
|
|
967
|
+
previewKeyBase,
|
|
968
|
+
}: AllVariantsPreviewProps) {
|
|
969
|
+
return (
|
|
970
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
|
|
971
|
+
{variants.map((variant, index) => {
|
|
972
|
+
const isFocused = index === focusedVariantIndex;
|
|
973
|
+
|
|
974
|
+
return (
|
|
975
|
+
<section
|
|
976
|
+
id={getVariantSectionId(componentName, variant.name)}
|
|
977
|
+
key={variant.name}
|
|
978
|
+
style={{
|
|
979
|
+
border: '1px solid var(--border)',
|
|
980
|
+
borderColor: isFocused ? 'var(--color-accent)' : 'var(--border)',
|
|
981
|
+
borderRadius: '10px',
|
|
982
|
+
overflow: 'hidden',
|
|
983
|
+
backgroundColor: 'var(--bg-primary)',
|
|
984
|
+
boxShadow: isFocused ? '0 0 0 1px color-mix(in srgb, var(--color-accent) 40%, transparent)' : undefined,
|
|
985
|
+
}}
|
|
986
|
+
>
|
|
987
|
+
<Stack direction="row" align="baseline" gap="sm" style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
|
|
988
|
+
<Text size="sm" weight="medium">{variant.name}</Text>
|
|
989
|
+
<Text size="xs" color="secondary">{variant.description}</Text>
|
|
990
|
+
</Stack>
|
|
991
|
+
<PreviewArea
|
|
992
|
+
componentName={componentName}
|
|
993
|
+
fragmentPath={fragmentPath}
|
|
994
|
+
variant={variant}
|
|
995
|
+
variants={variants}
|
|
996
|
+
zoom={zoom}
|
|
997
|
+
background={background}
|
|
998
|
+
viewport={viewport}
|
|
999
|
+
customSize={customSize}
|
|
1000
|
+
previewTheme={previewTheme}
|
|
1001
|
+
showMatrixView={false}
|
|
1002
|
+
showMultiViewport={false}
|
|
1003
|
+
showComparison={showComparison}
|
|
1004
|
+
figmaUrl={variant.figma || fallbackFigmaUrl}
|
|
1005
|
+
allFigmaUrls={allFigmaUrls}
|
|
1006
|
+
onSelectVariant={() => {}}
|
|
1007
|
+
onRetry={onRetry}
|
|
1008
|
+
renderContent={() => renderVariantContent(variant)}
|
|
1009
|
+
previewKey={`${previewKeyBase}-${index}`}
|
|
1010
|
+
/>
|
|
1011
|
+
</section>
|
|
1012
|
+
);
|
|
1013
|
+
})}
|
|
663
1014
|
</div>
|
|
664
1015
|
);
|
|
665
1016
|
}
|