@fragments-sdk/cli 0.7.10 → 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 +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 +475 -119
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -1
- package/src/viewer/components/CodePanel.tsx +0 -1
- package/src/viewer/components/Layout.tsx +16 -13
- package/src/viewer/components/PreviewArea.tsx +21 -11
- 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 () => {
|
|
@@ -216,7 +284,6 @@ export function App({ fragments }: AppProps) {
|
|
|
216
284
|
}, [fragments]);
|
|
217
285
|
|
|
218
286
|
const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || '');
|
|
219
|
-
const variantCount = activeFragment?.fragment.variants?.length || 0;
|
|
220
287
|
|
|
221
288
|
// Keyboard shortcuts
|
|
222
289
|
useKeyboardShortcuts(
|
|
@@ -229,9 +296,32 @@ export function App({ fragments }: AppProps) {
|
|
|
229
296
|
const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
|
|
230
297
|
handleSelectFragment(sortedFragmentPaths[prevIndex]);
|
|
231
298
|
},
|
|
232
|
-
nextVariant: () =>
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
},
|
|
235
325
|
toggleTheme: viewSettings.toggleTheme,
|
|
236
326
|
togglePanel: uiActions.togglePanel,
|
|
237
327
|
toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
|
|
@@ -255,22 +345,22 @@ export function App({ fragments }: AppProps) {
|
|
|
255
345
|
);
|
|
256
346
|
|
|
257
347
|
// Render variant with action logging via DOM event capture
|
|
258
|
-
const renderVariantWithProps = useCallback(() => {
|
|
259
|
-
if (!
|
|
348
|
+
const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
|
|
349
|
+
if (!variant) return null;
|
|
260
350
|
|
|
261
351
|
return (
|
|
262
352
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
263
|
-
<StoryRenderer variant={
|
|
353
|
+
<StoryRenderer variant={variant}>
|
|
264
354
|
{(content, isLoading, error) => {
|
|
265
355
|
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={
|
|
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." />;
|
|
268
358
|
return content;
|
|
269
359
|
}}
|
|
270
360
|
</StoryRenderer>
|
|
271
361
|
</ActionCapture>
|
|
272
362
|
);
|
|
273
|
-
}, [
|
|
363
|
+
}, []);
|
|
274
364
|
|
|
275
365
|
// Check if isolated mode
|
|
276
366
|
const isIsolated = useMemo(() => {
|
|
@@ -301,24 +391,12 @@ export function App({ fragments }: AppProps) {
|
|
|
301
391
|
activeFragment && !uiState.showHealthDashboard ? (
|
|
302
392
|
<TopToolbar
|
|
303
393
|
fragment={activeFragment}
|
|
304
|
-
variant={activeVariant}
|
|
305
|
-
viewSettings={viewSettings}
|
|
306
394
|
uiState={uiState}
|
|
307
395
|
uiActions={uiActions}
|
|
308
396
|
figmaUrl={figmaUrl}
|
|
309
397
|
searchQuery={searchQuery}
|
|
310
398
|
onSearchChange={setSearchQuery}
|
|
311
399
|
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
400
|
/>
|
|
323
401
|
) : (
|
|
324
402
|
<ViewerHeader
|
|
@@ -342,6 +420,21 @@ export function App({ fragments }: AppProps) {
|
|
|
342
420
|
}}
|
|
343
421
|
/>
|
|
344
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
|
+
}
|
|
345
438
|
>
|
|
346
439
|
{uiState.showHealthDashboard ? (
|
|
347
440
|
<div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
|
|
@@ -359,21 +452,25 @@ export function App({ fragments }: AppProps) {
|
|
|
359
452
|
</Box>
|
|
360
453
|
</div>
|
|
361
454
|
) : activeFragment ? (
|
|
362
|
-
<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' }}>
|
|
363
456
|
{/* Main Content Area */}
|
|
364
457
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
/>
|
|
374
470
|
|
|
375
471
|
{/* Preview Area */}
|
|
376
472
|
<div
|
|
473
|
+
id="preview-canvas"
|
|
377
474
|
style={{
|
|
378
475
|
flex: 1,
|
|
379
476
|
overflow: 'auto',
|
|
@@ -381,12 +478,32 @@ export function App({ fragments }: AppProps) {
|
|
|
381
478
|
...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
|
|
382
479
|
}}
|
|
383
480
|
>
|
|
384
|
-
{
|
|
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
|
+
) : (
|
|
385
502
|
<PreviewArea
|
|
386
503
|
componentName={activeFragment.fragment.meta.name}
|
|
387
504
|
fragmentPath={activeFragment.path}
|
|
388
505
|
variant={activeVariant}
|
|
389
|
-
variants={
|
|
506
|
+
variants={variants}
|
|
390
507
|
zoom={viewSettings.zoom}
|
|
391
508
|
background={viewSettings.background}
|
|
392
509
|
viewport={viewSettings.viewport}
|
|
@@ -402,40 +519,40 @@ export function App({ fragments }: AppProps) {
|
|
|
402
519
|
handleSelectVariant(index);
|
|
403
520
|
}}
|
|
404
521
|
onRetry={uiActions.incrementPreviewKey}
|
|
405
|
-
renderContent={renderVariantWithProps}
|
|
406
|
-
previewKey={`${activeFragmentPath}-${
|
|
522
|
+
renderContent={() => renderVariantWithProps(activeVariant)}
|
|
523
|
+
previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
|
|
407
524
|
/>
|
|
408
|
-
) : (
|
|
409
|
-
<NoVariantsMessage fragment={activeFragment?.fragment} />
|
|
410
525
|
)}
|
|
411
526
|
</div>
|
|
412
527
|
</div>
|
|
413
528
|
|
|
414
529
|
{/* 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
|
-
|
|
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>
|
|
439
556
|
</div>
|
|
440
557
|
) : (
|
|
441
558
|
<EmptyState style={{ height: '100%' }}>
|
|
@@ -454,18 +571,12 @@ export function App({ fragments }: AppProps) {
|
|
|
454
571
|
// Top Toolbar Component
|
|
455
572
|
interface TopToolbarProps {
|
|
456
573
|
fragment: { path: string; fragment: FragmentDefinition };
|
|
457
|
-
variant: any;
|
|
458
|
-
viewSettings: ReturnType<typeof useViewSettings>;
|
|
459
574
|
uiState: ReturnType<typeof useAppState>['state'];
|
|
460
575
|
uiActions: ReturnType<typeof useAppState>['actions'];
|
|
461
576
|
figmaUrl?: string;
|
|
462
577
|
searchQuery: string;
|
|
463
578
|
onSearchChange: (value: string) => void;
|
|
464
579
|
searchInputRef: RefObject<HTMLInputElement>;
|
|
465
|
-
onPrevComponent: () => void;
|
|
466
|
-
onNextComponent: () => void;
|
|
467
|
-
onPrevVariant: () => void;
|
|
468
|
-
onNextVariant: () => void;
|
|
469
580
|
}
|
|
470
581
|
|
|
471
582
|
interface ViewerHeaderProps {
|
|
@@ -481,6 +592,18 @@ interface HeaderSearchProps {
|
|
|
481
592
|
inputRef: RefObject<HTMLInputElement>;
|
|
482
593
|
}
|
|
483
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
|
+
|
|
484
607
|
function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
485
608
|
return (
|
|
486
609
|
<Header.Search expandable>
|
|
@@ -542,43 +665,121 @@ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef
|
|
|
542
665
|
);
|
|
543
666
|
}
|
|
544
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
|
+
|
|
545
770
|
function TopToolbar({
|
|
546
771
|
fragment,
|
|
547
|
-
variant,
|
|
548
|
-
viewSettings,
|
|
549
772
|
uiState,
|
|
550
773
|
uiActions,
|
|
551
774
|
figmaUrl,
|
|
552
775
|
searchQuery,
|
|
553
776
|
onSearchChange,
|
|
554
777
|
searchInputRef,
|
|
555
|
-
onPrevComponent,
|
|
556
|
-
onNextComponent,
|
|
557
|
-
onPrevVariant,
|
|
558
|
-
onNextVariant,
|
|
559
778
|
}: TopToolbarProps) {
|
|
560
779
|
const { setTheme, resolvedTheme } = useTheme();
|
|
561
780
|
return (
|
|
562
781
|
<Header aria-label="Component preview toolbar">
|
|
563
782
|
<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
783
|
<Header.Brand>
|
|
583
784
|
<Stack direction="row" align="center" gap="sm">
|
|
584
785
|
<img src={fragmentsLogo} alt="" width={20} height={20} style={{ display: 'block' }} />
|
|
@@ -642,24 +843,179 @@ function TopToolbar({
|
|
|
642
843
|
);
|
|
643
844
|
}
|
|
644
845
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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"];
|
|
650
864
|
showMatrixView: boolean;
|
|
865
|
+
showMultiViewport: boolean;
|
|
866
|
+
panelOpen: boolean;
|
|
867
|
+
onToggleMatrix: () => void;
|
|
868
|
+
onToggleMultiViewport: () => void;
|
|
869
|
+
onTogglePanel: () => void;
|
|
651
870
|
}
|
|
652
871
|
|
|
653
|
-
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
|
+
|
|
654
888
|
return (
|
|
655
889
|
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
|
|
656
|
-
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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"}>
|
|
899
|
+
<Button
|
|
900
|
+
variant="ghost"
|
|
901
|
+
size="sm"
|
|
902
|
+
aria-pressed={showMatrixView}
|
|
903
|
+
aria-label="Toggle matrix view"
|
|
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
|
|
912
|
+
variant="ghost"
|
|
913
|
+
size="sm"
|
|
914
|
+
aria-pressed={showMultiViewport}
|
|
915
|
+
aria-label="Toggle responsive view"
|
|
916
|
+
onClick={onToggleMultiViewport}
|
|
917
|
+
style={toggleButtonStyle(showMultiViewport)}
|
|
918
|
+
>
|
|
919
|
+
<DeviceMobile size={16} />
|
|
920
|
+
</Button>
|
|
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>
|
|
934
|
+
</Stack>
|
|
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
|
+
})}
|
|
663
1019
|
</div>
|
|
664
1020
|
);
|
|
665
1021
|
}
|