@fragments-sdk/cli 0.7.11 → 0.7.13
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/package.json +1 -1
- package/src/viewer/components/App.tsx +49 -111
- package/src/viewer/components/CommandPalette.tsx +17 -1
- package/src/viewer/components/IsolatedPreviewFrame.tsx +71 -74
- package/src/viewer/components/IsolatedRender.tsx +3 -7
- package/src/viewer/components/LeftSidebar.tsx +23 -3
- package/src/viewer/components/MultiViewportPreview.tsx +0 -17
- package/src/viewer/components/PreviewArea.tsx +5 -15
- package/src/viewer/components/PreviewToolbar.tsx +3 -103
- package/src/viewer/components/VariantMatrix.tsx +0 -11
- package/src/viewer/constants/ui.ts +0 -34
- package/src/viewer/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/viewer/hooks/useUrlState.ts +2 -14
- package/src/viewer/hooks/useViewSettings.ts +4 -17
package/package.json
CHANGED
|
@@ -15,7 +15,6 @@ import { useToast } from "./Toast.js";
|
|
|
15
15
|
|
|
16
16
|
// Toolbar
|
|
17
17
|
import { PreviewToolbar } from "./PreviewToolbar.js";
|
|
18
|
-
import { getBackgroundStyle } from "../constants/ui.js";
|
|
19
18
|
|
|
20
19
|
// Preview & Rendering
|
|
21
20
|
import { PreviewArea } from "./PreviewArea.js";
|
|
@@ -65,16 +64,14 @@ export function App({ fragments }: AppProps) {
|
|
|
65
64
|
// UI state (modals, panels, view modes)
|
|
66
65
|
const { state: uiState, actions: uiActions } = useAppState();
|
|
67
66
|
|
|
68
|
-
// View settings (zoom,
|
|
67
|
+
// View settings (zoom, viewport, theme)
|
|
69
68
|
const viewSettings = useViewSettings({
|
|
70
69
|
initialState: {
|
|
71
70
|
zoom: urlState.zoom as any,
|
|
72
|
-
background: urlState.background as any,
|
|
73
71
|
viewport: urlState.viewport as any,
|
|
74
72
|
customSize: { width: urlState.customWidth, height: urlState.customHeight },
|
|
75
73
|
},
|
|
76
74
|
onZoomChange: (zoom) => setUrlViewSettings({ zoom }),
|
|
77
|
-
onBackgroundChange: (bg) => setUrlViewSettings({ background: bg }),
|
|
78
75
|
onViewportChange: (vp, size) => setUrlViewSettings({
|
|
79
76
|
viewport: vp,
|
|
80
77
|
customWidth: size?.width,
|
|
@@ -270,11 +267,6 @@ export function App({ fragments }: AppProps) {
|
|
|
270
267
|
}
|
|
271
268
|
}, [copyUrl, success, uiActions]);
|
|
272
269
|
|
|
273
|
-
const focusSearchInput = useCallback(() => {
|
|
274
|
-
searchInputRef.current?.focus();
|
|
275
|
-
searchInputRef.current?.select();
|
|
276
|
-
}, []);
|
|
277
|
-
|
|
278
270
|
// Sorted fragment paths for keyboard navigation
|
|
279
271
|
const sortedFragmentPaths = useMemo(() => {
|
|
280
272
|
return [...fragments]
|
|
@@ -328,7 +320,7 @@ export function App({ fragments }: AppProps) {
|
|
|
328
320
|
toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
|
|
329
321
|
copyLink: handleCopyLink,
|
|
330
322
|
showHelp: uiActions.toggleShortcutsHelp,
|
|
331
|
-
openSearch:
|
|
323
|
+
openSearch: () => uiActions.setCommandPalette(true),
|
|
332
324
|
escape: () => {
|
|
333
325
|
if (document.activeElement === searchInputRef.current) {
|
|
334
326
|
if (searchQuery) {
|
|
@@ -391,6 +383,7 @@ export function App({ fragments }: AppProps) {
|
|
|
391
383
|
activeFragment && !uiState.showHealthDashboard ? (
|
|
392
384
|
<TopToolbar
|
|
393
385
|
fragment={activeFragment}
|
|
386
|
+
viewSettings={viewSettings}
|
|
394
387
|
uiState={uiState}
|
|
395
388
|
uiActions={uiActions}
|
|
396
389
|
figmaUrl={figmaUrl}
|
|
@@ -455,19 +448,6 @@ export function App({ fragments }: AppProps) {
|
|
|
455
448
|
<div id="preview-layout" style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
|
|
456
449
|
{/* Main Content Area */}
|
|
457
450
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
|
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
|
-
/>
|
|
470
|
-
|
|
471
451
|
{/* Preview Area */}
|
|
472
452
|
<div
|
|
473
453
|
id="preview-canvas"
|
|
@@ -475,7 +455,6 @@ export function App({ fragments }: AppProps) {
|
|
|
475
455
|
flex: 1,
|
|
476
456
|
overflow: 'auto',
|
|
477
457
|
position: 'relative',
|
|
478
|
-
...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
|
|
479
458
|
}}
|
|
480
459
|
>
|
|
481
460
|
{variantCount === 0 ? (
|
|
@@ -487,7 +466,6 @@ export function App({ fragments }: AppProps) {
|
|
|
487
466
|
variants={variants}
|
|
488
467
|
focusedVariantIndex={safeVariantIndex}
|
|
489
468
|
zoom={viewSettings.zoom}
|
|
490
|
-
background={viewSettings.background}
|
|
491
469
|
viewport={viewSettings.viewport}
|
|
492
470
|
customSize={viewSettings.customSize}
|
|
493
471
|
previewTheme={resolvedTheme}
|
|
@@ -505,7 +483,6 @@ export function App({ fragments }: AppProps) {
|
|
|
505
483
|
variant={activeVariant}
|
|
506
484
|
variants={variants}
|
|
507
485
|
zoom={viewSettings.zoom}
|
|
508
|
-
background={viewSettings.background}
|
|
509
486
|
viewport={viewSettings.viewport}
|
|
510
487
|
customSize={viewSettings.customSize}
|
|
511
488
|
previewTheme={resolvedTheme}
|
|
@@ -571,6 +548,7 @@ export function App({ fragments }: AppProps) {
|
|
|
571
548
|
// Top Toolbar Component
|
|
572
549
|
interface TopToolbarProps {
|
|
573
550
|
fragment: { path: string; fragment: FragmentDefinition };
|
|
551
|
+
viewSettings: ReturnType<typeof useViewSettings>;
|
|
574
552
|
uiState: ReturnType<typeof useAppState>['state'];
|
|
575
553
|
uiActions: ReturnType<typeof useAppState>['actions'];
|
|
576
554
|
figmaUrl?: string;
|
|
@@ -614,7 +592,6 @@ function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
|
614
592
|
placeholder="Search components"
|
|
615
593
|
aria-label="Search components"
|
|
616
594
|
size="sm"
|
|
617
|
-
shortcut="⌘K"
|
|
618
595
|
style={{ width: '240px' }}
|
|
619
596
|
/>
|
|
620
597
|
</Header.Search>
|
|
@@ -723,11 +700,12 @@ function PreviewAside({
|
|
|
723
700
|
</a>
|
|
724
701
|
{variants.map((variant, index) => {
|
|
725
702
|
const active = index === focusedVariantIndex;
|
|
703
|
+
const anchorId = getVariantSectionId(fragment.meta.name, variant.name);
|
|
726
704
|
|
|
727
705
|
return (
|
|
728
706
|
<a
|
|
729
707
|
key={variant.name}
|
|
730
|
-
href=
|
|
708
|
+
href={`#${anchorId}`}
|
|
731
709
|
style={getLinkStyle(active)}
|
|
732
710
|
onClick={(event) => {
|
|
733
711
|
event.preventDefault();
|
|
@@ -769,6 +747,7 @@ function PreviewAside({
|
|
|
769
747
|
|
|
770
748
|
function TopToolbar({
|
|
771
749
|
fragment,
|
|
750
|
+
viewSettings,
|
|
772
751
|
uiState,
|
|
773
752
|
uiActions,
|
|
774
753
|
figmaUrl,
|
|
@@ -790,6 +769,48 @@ function TopToolbar({
|
|
|
790
769
|
<HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
|
|
791
770
|
<Header.Spacer />
|
|
792
771
|
<Header.Actions>
|
|
772
|
+
<PreviewToolbar
|
|
773
|
+
zoom={viewSettings.zoom}
|
|
774
|
+
onZoomChange={viewSettings.setZoom}
|
|
775
|
+
/>
|
|
776
|
+
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
777
|
+
<Tooltip content={uiState.showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
|
|
778
|
+
<Button
|
|
779
|
+
variant="ghost"
|
|
780
|
+
size="sm"
|
|
781
|
+
aria-pressed={uiState.showMatrixView}
|
|
782
|
+
aria-label="Toggle matrix view"
|
|
783
|
+
onClick={() => uiActions.setMatrixView(!uiState.showMatrixView)}
|
|
784
|
+
style={uiState.showMatrixView ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
785
|
+
>
|
|
786
|
+
<GridFour size={16} />
|
|
787
|
+
</Button>
|
|
788
|
+
</Tooltip>
|
|
789
|
+
<Tooltip content={uiState.showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
|
|
790
|
+
<Button
|
|
791
|
+
variant="ghost"
|
|
792
|
+
size="sm"
|
|
793
|
+
aria-pressed={uiState.showMultiViewport}
|
|
794
|
+
aria-label="Toggle responsive view"
|
|
795
|
+
onClick={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
|
|
796
|
+
style={uiState.showMultiViewport ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
797
|
+
>
|
|
798
|
+
<DeviceMobile size={16} />
|
|
799
|
+
</Button>
|
|
800
|
+
</Tooltip>
|
|
801
|
+
<Tooltip content={uiState.panelOpen ? "Hide addons panel" : "Show addons panel"}>
|
|
802
|
+
<Button
|
|
803
|
+
variant="ghost"
|
|
804
|
+
size="sm"
|
|
805
|
+
aria-pressed={uiState.panelOpen}
|
|
806
|
+
aria-label="Toggle addons panel"
|
|
807
|
+
onClick={uiActions.togglePanel}
|
|
808
|
+
style={uiState.panelOpen ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
|
|
809
|
+
>
|
|
810
|
+
<Rows size={16} />
|
|
811
|
+
</Button>
|
|
812
|
+
</Tooltip>
|
|
813
|
+
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
793
814
|
{figmaUrl && (
|
|
794
815
|
<>
|
|
795
816
|
<Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
|
|
@@ -856,93 +877,12 @@ function getVariantSectionId(componentName: string, variantName: string): string
|
|
|
856
877
|
return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
|
|
857
878
|
}
|
|
858
879
|
|
|
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"];
|
|
864
|
-
showMatrixView: boolean;
|
|
865
|
-
showMultiViewport: boolean;
|
|
866
|
-
panelOpen: boolean;
|
|
867
|
-
onToggleMatrix: () => void;
|
|
868
|
-
onToggleMultiViewport: () => void;
|
|
869
|
-
onTogglePanel: () => void;
|
|
870
|
-
}
|
|
871
|
-
|
|
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
|
-
|
|
888
|
-
return (
|
|
889
|
-
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
|
|
890
|
-
<Stack direction="row" gap="sm" align="center" justify="end">
|
|
891
|
-
<PreviewToolbar
|
|
892
|
-
zoom={zoom}
|
|
893
|
-
background={background}
|
|
894
|
-
onZoomChange={onZoomChange}
|
|
895
|
-
onBackgroundChange={onBackgroundChange}
|
|
896
|
-
/>
|
|
897
|
-
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
898
|
-
<Tooltip content={showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
|
|
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
880
|
interface AllVariantsPreviewProps {
|
|
940
881
|
componentName: string;
|
|
941
882
|
fragmentPath: string;
|
|
942
883
|
variants: FragmentVariant[];
|
|
943
884
|
focusedVariantIndex: number;
|
|
944
885
|
zoom: ReturnType<typeof useViewSettings>["zoom"];
|
|
945
|
-
background: ReturnType<typeof useViewSettings>["background"];
|
|
946
886
|
viewport: ReturnType<typeof useViewSettings>["viewport"];
|
|
947
887
|
customSize: ReturnType<typeof useViewSettings>["customSize"];
|
|
948
888
|
previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
|
|
@@ -960,7 +900,6 @@ function AllVariantsPreview({
|
|
|
960
900
|
variants,
|
|
961
901
|
focusedVariantIndex,
|
|
962
902
|
zoom,
|
|
963
|
-
background,
|
|
964
903
|
viewport,
|
|
965
904
|
customSize,
|
|
966
905
|
previewTheme,
|
|
@@ -999,7 +938,6 @@ function AllVariantsPreview({
|
|
|
999
938
|
variant={variant}
|
|
1000
939
|
variants={variants}
|
|
1001
940
|
zoom={zoom}
|
|
1002
|
-
background={background}
|
|
1003
941
|
viewport={viewport}
|
|
1004
942
|
customSize={customSize}
|
|
1005
943
|
previewTheme={previewTheme}
|
|
@@ -336,10 +336,16 @@ export function CommandPalette({
|
|
|
336
336
|
function fuzzyScore(text: string, query: string): number {
|
|
337
337
|
if (!query) return 1;
|
|
338
338
|
|
|
339
|
+
// Require substring match for short queries to avoid scattered single-char noise
|
|
340
|
+
if (query.length <= 3 && !text.includes(query)) {
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
339
344
|
let score = 0;
|
|
340
345
|
let queryIndex = 0;
|
|
341
346
|
let consecutiveBonus = 0;
|
|
342
347
|
let lastMatchIndex = -2;
|
|
348
|
+
let maxGap = 0;
|
|
343
349
|
|
|
344
350
|
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
|
|
345
351
|
if (text[i] === query[queryIndex]) {
|
|
@@ -351,6 +357,11 @@ function fuzzyScore(text: string, query: string): number {
|
|
|
351
357
|
score += consecutiveBonus;
|
|
352
358
|
} else {
|
|
353
359
|
consecutiveBonus = 0;
|
|
360
|
+
// Track max gap between matches
|
|
361
|
+
if (lastMatchIndex >= 0) {
|
|
362
|
+
const gap = i - lastMatchIndex;
|
|
363
|
+
if (gap > maxGap) maxGap = gap;
|
|
364
|
+
}
|
|
354
365
|
}
|
|
355
366
|
|
|
356
367
|
// Bonus for matching at word start
|
|
@@ -366,6 +377,11 @@ function fuzzyScore(text: string, query: string): number {
|
|
|
366
377
|
// Only return score if all query characters were found
|
|
367
378
|
if (queryIndex < query.length) return 0;
|
|
368
379
|
|
|
380
|
+
// Penalize large gaps between matched characters
|
|
381
|
+
if (maxGap > 5) {
|
|
382
|
+
score -= maxGap;
|
|
383
|
+
}
|
|
384
|
+
|
|
369
385
|
// Bonus for shorter results (more relevant)
|
|
370
386
|
score += Math.max(0, 20 - text.length);
|
|
371
387
|
|
|
@@ -375,5 +391,5 @@ function fuzzyScore(text: string, query: string): number {
|
|
|
375
391
|
// Bonus for starts with
|
|
376
392
|
if (text.startsWith(query)) score += 30;
|
|
377
393
|
|
|
378
|
-
return score;
|
|
394
|
+
return Math.max(score, 0) || 0;
|
|
379
395
|
}
|
|
@@ -162,91 +162,88 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
162
162
|
onMouseLeave={() => setIsHovered(false)}
|
|
163
163
|
>
|
|
164
164
|
{/* Skeleton loading overlay (initial load) */}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
</div>
|
|
165
|
+
{showSkeleton && (
|
|
166
|
+
<div
|
|
167
|
+
style={{
|
|
168
|
+
position: 'absolute',
|
|
169
|
+
inset: 0,
|
|
170
|
+
zIndex: 10,
|
|
171
|
+
background: 'var(--bg-primary, rgba(255, 255, 255, 0.95))',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<PreviewSkeleton />
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
178
177
|
|
|
179
178
|
{/* Spinner overlay (subsequent renders) */}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<span>Rendering...</span>
|
|
179
|
+
{showSpinner && (
|
|
180
|
+
<div
|
|
181
|
+
style={{
|
|
182
|
+
position: 'absolute',
|
|
183
|
+
inset: 0,
|
|
184
|
+
zIndex: 10,
|
|
185
|
+
display: 'flex',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
background: 'color-mix(in srgb, var(--bg-primary, white) 80%, transparent)',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-tertiary, #6b7280)', fontSize: 14 }}>
|
|
192
|
+
<LoadingSpinner />
|
|
193
|
+
<span>Rendering...</span>
|
|
194
|
+
</div>
|
|
197
195
|
</div>
|
|
198
|
-
|
|
196
|
+
)}
|
|
199
197
|
|
|
200
198
|
{/* Error overlay */}
|
|
201
|
-
|
|
202
|
-
style={{
|
|
203
|
-
position: 'absolute',
|
|
204
|
-
inset: 0,
|
|
205
|
-
zIndex: 10,
|
|
206
|
-
display: 'flex',
|
|
207
|
-
alignItems: 'center',
|
|
208
|
-
justifyContent: 'center',
|
|
209
|
-
padding: '16px',
|
|
210
|
-
transition: 'opacity 150ms',
|
|
211
|
-
opacity: frameError && !isLoading ? 1 : 0,
|
|
212
|
-
pointerEvents: frameError && !isLoading ? 'auto' : 'none',
|
|
213
|
-
background: 'rgba(254, 242, 242, 0.95)',
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
199
|
+
{frameError && !isLoading && (
|
|
216
200
|
<div
|
|
217
201
|
style={{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
202
|
+
position: 'absolute',
|
|
203
|
+
inset: 0,
|
|
204
|
+
zIndex: 10,
|
|
205
|
+
display: 'flex',
|
|
206
|
+
alignItems: 'center',
|
|
207
|
+
justifyContent: 'center',
|
|
208
|
+
padding: '16px',
|
|
209
|
+
background: 'rgba(254, 242, 242, 0.95)',
|
|
223
210
|
}}
|
|
224
211
|
>
|
|
225
|
-
<div
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
212
|
+
<div
|
|
213
|
+
style={{
|
|
214
|
+
background: 'white',
|
|
215
|
+
border: '1px solid #fecaca',
|
|
216
|
+
borderRadius: 8,
|
|
217
|
+
padding: 16,
|
|
218
|
+
maxWidth: 400,
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<div style={{ color: '#dc2626', fontWeight: 500, marginBottom: 8 }}>
|
|
222
|
+
Preview Error
|
|
223
|
+
</div>
|
|
224
|
+
<div style={{ color: '#991b1b', fontSize: 13, marginBottom: retryCount < MAX_RETRIES ? 12 : 0 }}>
|
|
225
|
+
{frameError}
|
|
226
|
+
</div>
|
|
227
|
+
{retryCount < MAX_RETRIES && (
|
|
228
|
+
<button
|
|
229
|
+
onClick={handleRetry}
|
|
230
|
+
style={{
|
|
231
|
+
padding: '6px 12px',
|
|
232
|
+
fontSize: 13,
|
|
233
|
+
fontWeight: 500,
|
|
234
|
+
color: 'white',
|
|
235
|
+
background: '#dc2626',
|
|
236
|
+
border: 'none',
|
|
237
|
+
borderRadius: 6,
|
|
238
|
+
cursor: 'pointer',
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
Retry ({MAX_RETRIES - retryCount} remaining)
|
|
242
|
+
</button>
|
|
243
|
+
)}
|
|
230
244
|
</div>
|
|
231
|
-
{retryCount < MAX_RETRIES && (
|
|
232
|
-
<button
|
|
233
|
-
onClick={handleRetry}
|
|
234
|
-
style={{
|
|
235
|
-
padding: '6px 12px',
|
|
236
|
-
fontSize: 13,
|
|
237
|
-
fontWeight: 500,
|
|
238
|
-
color: 'white',
|
|
239
|
-
background: '#dc2626',
|
|
240
|
-
border: 'none',
|
|
241
|
-
borderRadius: 6,
|
|
242
|
-
cursor: 'pointer',
|
|
243
|
-
}}
|
|
244
|
-
>
|
|
245
|
-
Retry ({MAX_RETRIES - retryCount} remaining)
|
|
246
|
-
</button>
|
|
247
|
-
)}
|
|
248
245
|
</div>
|
|
249
|
-
|
|
246
|
+
)}
|
|
250
247
|
|
|
251
248
|
{/* The iframe */}
|
|
252
249
|
<iframe
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo, useEffect, useState } from "react";
|
|
2
2
|
import type { FragmentDefinition } from "../../core/index.js";
|
|
3
3
|
import { VariantRenderer } from "./VariantRenderer.js";
|
|
4
|
-
import {
|
|
4
|
+
import { type ZoomLevel } from "../constants/ui.js";
|
|
5
5
|
|
|
6
6
|
interface IsolatedRenderProps {
|
|
7
7
|
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
@@ -10,7 +10,7 @@ interface IsolatedRenderProps {
|
|
|
10
10
|
/**
|
|
11
11
|
* Isolated render component for screenshot capture and standalone viewing.
|
|
12
12
|
* Renders a single variant with minimal UI for visual testing.
|
|
13
|
-
* URL params: ?isolated=true&component=Name&variant=VariantName&zoom=100&
|
|
13
|
+
* URL params: ?isolated=true&component=Name&variant=VariantName&zoom=100&theme=light
|
|
14
14
|
*/
|
|
15
15
|
export function IsolatedRender({ fragments }: IsolatedRenderProps) {
|
|
16
16
|
const [ready, setReady] = useState(false);
|
|
@@ -19,15 +19,11 @@ export function IsolatedRender({ fragments }: IsolatedRenderProps) {
|
|
|
19
19
|
const params = useMemo(() => {
|
|
20
20
|
const searchParams = new URLSearchParams(window.location.search);
|
|
21
21
|
const zoomParam = parseInt(searchParams.get("zoom") || "100", 10);
|
|
22
|
-
const bgParam = searchParams.get("bg") || "white";
|
|
23
22
|
return {
|
|
24
23
|
component: searchParams.get("component"),
|
|
25
24
|
variant: searchParams.get("variant"),
|
|
26
25
|
theme: searchParams.get("theme") || "light",
|
|
27
26
|
zoom: [50, 75, 100, 150, 200].includes(zoomParam) ? zoomParam as ZoomLevel : 100 as ZoomLevel,
|
|
28
|
-
background: ["white", "black", "checkerboard", "transparent"].includes(bgParam)
|
|
29
|
-
? bgParam as BackgroundOption
|
|
30
|
-
: "white" as BackgroundOption,
|
|
31
27
|
};
|
|
32
28
|
}, []);
|
|
33
29
|
|
|
@@ -101,7 +97,7 @@ export function IsolatedRender({ fragments }: IsolatedRenderProps) {
|
|
|
101
97
|
display: 'flex',
|
|
102
98
|
alignItems: 'center',
|
|
103
99
|
justifyContent: 'center',
|
|
104
|
-
|
|
100
|
+
backgroundColor: 'var(--bg-primary)',
|
|
105
101
|
}}
|
|
106
102
|
>
|
|
107
103
|
<div
|
|
@@ -14,16 +14,26 @@ function fuzzyMatch(text: string, pattern: string): FuzzyMatch | null {
|
|
|
14
14
|
const textLower = text.toLowerCase();
|
|
15
15
|
const patternLower = pattern.toLowerCase();
|
|
16
16
|
|
|
17
|
+
// Require substring match for short queries (<=3 chars) to avoid scattered single-char noise
|
|
18
|
+
if (patternLower.length <= 3 && !textLower.includes(patternLower)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
const indices: number[] = [];
|
|
18
23
|
let patternIdx = 0;
|
|
19
24
|
let score = 0;
|
|
20
25
|
let consecutiveBonus = 0;
|
|
26
|
+
let maxGap = 0;
|
|
21
27
|
|
|
22
28
|
for (let i = 0; i < textLower.length && patternIdx < patternLower.length; i++) {
|
|
23
29
|
if (textLower[i] === patternLower[patternIdx]) {
|
|
24
30
|
indices.push(i);
|
|
25
|
-
if (indices.length > 1
|
|
26
|
-
|
|
31
|
+
if (indices.length > 1) {
|
|
32
|
+
const gap = i - indices[indices.length - 2];
|
|
33
|
+
if (gap === 1) {
|
|
34
|
+
consecutiveBonus += 5;
|
|
35
|
+
}
|
|
36
|
+
if (gap > maxGap) maxGap = gap;
|
|
27
37
|
}
|
|
28
38
|
if (i === 0 || text[i - 1] === ' ' || text[i - 1] === '-' || text[i - 1] === '_') {
|
|
29
39
|
score += 10;
|
|
@@ -36,9 +46,19 @@ function fuzzyMatch(text: string, pattern: string): FuzzyMatch | null {
|
|
|
36
46
|
return null;
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
// Penalize large gaps between matched characters
|
|
50
|
+
if (maxGap > 5) {
|
|
51
|
+
score -= maxGap * 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
score += consecutiveBonus;
|
|
40
55
|
score += (patternLower.length / textLower.length) * 20;
|
|
41
56
|
|
|
57
|
+
// Reject very low scores (scattered single-char matches)
|
|
58
|
+
if (score <= 0 && patternLower.length > 1) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
return { score, indices };
|
|
43
63
|
}
|
|
44
64
|
|
|
@@ -364,7 +384,7 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
|
|
|
364
384
|
<Sidebar.Footer>
|
|
365
385
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
|
366
386
|
<Text size="xs" color="tertiary">
|
|
367
|
-
{isFilterActive ? `${flatItems.length}
|
|
387
|
+
{isFilterActive || searchResults ? `${flatItems.length} of ${fragments.length}` : fragments.length} components
|
|
368
388
|
</Text>
|
|
369
389
|
<Sidebar.CollapseToggle />
|
|
370
390
|
</div>
|
|
@@ -10,7 +10,6 @@ import { useState, type ReactNode } from "react";
|
|
|
10
10
|
import { ErrorBoundary } from "./ErrorBoundary.js";
|
|
11
11
|
import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
|
|
12
12
|
import { ChevronDownIcon } from "./Icons.js";
|
|
13
|
-
import { getBackgroundStyle, type BackgroundOption } from "../constants/ui.js";
|
|
14
13
|
|
|
15
14
|
interface ViewportConfig {
|
|
16
15
|
name: string;
|
|
@@ -50,8 +49,6 @@ interface MultiViewportPreviewProps {
|
|
|
50
49
|
renderContent: () => ReactNode;
|
|
51
50
|
/** Preview theme */
|
|
52
51
|
previewTheme: "light" | "dark";
|
|
53
|
-
/** Background option for preview */
|
|
54
|
-
background: BackgroundOption;
|
|
55
52
|
/** Base zoom level (used for scaling if needed) */
|
|
56
53
|
zoom: number;
|
|
57
54
|
/** Whether to use iframe isolation */
|
|
@@ -64,7 +61,6 @@ export function MultiViewportPreview({
|
|
|
64
61
|
variantName,
|
|
65
62
|
renderContent,
|
|
66
63
|
previewTheme,
|
|
67
|
-
background,
|
|
68
64
|
zoom,
|
|
69
65
|
useIframeIsolation = true,
|
|
70
66
|
}: MultiViewportPreviewProps) {
|
|
@@ -207,7 +203,6 @@ export function MultiViewportPreview({
|
|
|
207
203
|
variantName={variantName}
|
|
208
204
|
renderContent={renderContent}
|
|
209
205
|
previewTheme={previewTheme}
|
|
210
|
-
background={background}
|
|
211
206
|
useIframeIsolation={useIframeIsolation}
|
|
212
207
|
/>
|
|
213
208
|
))}
|
|
@@ -224,7 +219,6 @@ interface ViewportPanelProps {
|
|
|
224
219
|
variantName: string;
|
|
225
220
|
renderContent: () => ReactNode;
|
|
226
221
|
previewTheme: "light" | "dark";
|
|
227
|
-
background: BackgroundOption;
|
|
228
222
|
useIframeIsolation: boolean;
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -235,7 +229,6 @@ function ViewportPanel({
|
|
|
235
229
|
variantName,
|
|
236
230
|
renderContent,
|
|
237
231
|
previewTheme,
|
|
238
|
-
background,
|
|
239
232
|
useIframeIsolation,
|
|
240
233
|
}: ViewportPanelProps) {
|
|
241
234
|
if (viewport.type === "desktop") {
|
|
@@ -245,7 +238,6 @@ function ViewportPanel({
|
|
|
245
238
|
height={viewport.height}
|
|
246
239
|
label={viewport.name}
|
|
247
240
|
previewTheme={previewTheme}
|
|
248
|
-
background={background}
|
|
249
241
|
componentName={componentName}
|
|
250
242
|
fragmentPath={fragmentPath}
|
|
251
243
|
variantName={variantName}
|
|
@@ -262,7 +254,6 @@ function ViewportPanel({
|
|
|
262
254
|
height={viewport.height}
|
|
263
255
|
label={viewport.name}
|
|
264
256
|
previewTheme={previewTheme}
|
|
265
|
-
background={background}
|
|
266
257
|
componentName={componentName}
|
|
267
258
|
fragmentPath={fragmentPath}
|
|
268
259
|
variantName={variantName}
|
|
@@ -278,7 +269,6 @@ interface DeviceMockupProps {
|
|
|
278
269
|
height: number;
|
|
279
270
|
label: string;
|
|
280
271
|
previewTheme: "light" | "dark";
|
|
281
|
-
background: BackgroundOption;
|
|
282
272
|
componentName: string;
|
|
283
273
|
fragmentPath: string;
|
|
284
274
|
variantName: string;
|
|
@@ -292,7 +282,6 @@ function DeviceMockup({
|
|
|
292
282
|
height,
|
|
293
283
|
label,
|
|
294
284
|
previewTheme,
|
|
295
|
-
background,
|
|
296
285
|
componentName,
|
|
297
286
|
fragmentPath,
|
|
298
287
|
variantName,
|
|
@@ -302,7 +291,6 @@ function DeviceMockup({
|
|
|
302
291
|
const isMobile = type === "mobile";
|
|
303
292
|
const frameWidth = width + 24; // Add bezel width
|
|
304
293
|
const screenHeight = height;
|
|
305
|
-
const backgroundStyle = getBackgroundStyle(background);
|
|
306
294
|
|
|
307
295
|
return (
|
|
308
296
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
@@ -479,7 +467,6 @@ interface DesktopMockupProps {
|
|
|
479
467
|
height: number;
|
|
480
468
|
label: string;
|
|
481
469
|
previewTheme: "light" | "dark";
|
|
482
|
-
background: BackgroundOption;
|
|
483
470
|
componentName: string;
|
|
484
471
|
fragmentPath: string;
|
|
485
472
|
variantName: string;
|
|
@@ -492,15 +479,12 @@ function DesktopMockup({
|
|
|
492
479
|
height,
|
|
493
480
|
label,
|
|
494
481
|
previewTheme,
|
|
495
|
-
background,
|
|
496
482
|
componentName,
|
|
497
483
|
fragmentPath,
|
|
498
484
|
variantName,
|
|
499
485
|
renderContent,
|
|
500
486
|
useIframeIsolation,
|
|
501
487
|
}: DesktopMockupProps) {
|
|
502
|
-
const backgroundStyle = getBackgroundStyle(background);
|
|
503
|
-
|
|
504
488
|
return (
|
|
505
489
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
506
490
|
{/* Label */}
|
|
@@ -568,7 +552,6 @@ function DesktopMockup({
|
|
|
568
552
|
overflow: 'hidden',
|
|
569
553
|
width: `${width}px`,
|
|
570
554
|
height: `${height}px`,
|
|
571
|
-
...backgroundStyle,
|
|
572
555
|
}}
|
|
573
556
|
data-theme={previewTheme}
|
|
574
557
|
>
|
|
@@ -13,7 +13,7 @@ import { FigmaEmbed } from './FigmaEmbed.js';
|
|
|
13
13
|
import { VariantMatrix } from './VariantMatrix.js';
|
|
14
14
|
import { MultiViewportPreview } from './MultiViewportPreview.js';
|
|
15
15
|
import { IsolatedPreviewFrame } from './IsolatedPreviewFrame.js';
|
|
16
|
-
import {
|
|
16
|
+
import { getViewportWidth, type ZoomLevel, type ViewportPreset, type ViewportSize } from '../constants/ui.js';
|
|
17
17
|
import type { PreviewTheme } from '../hooks/useViewSettings.js';
|
|
18
18
|
|
|
19
19
|
interface PreviewAreaProps {
|
|
@@ -25,7 +25,6 @@ interface PreviewAreaProps {
|
|
|
25
25
|
|
|
26
26
|
// View settings
|
|
27
27
|
zoom: ZoomLevel;
|
|
28
|
-
background: BackgroundOption;
|
|
29
28
|
viewport: ViewportPreset;
|
|
30
29
|
customSize: ViewportSize;
|
|
31
30
|
previewTheme: PreviewTheme;
|
|
@@ -151,11 +150,10 @@ const DeviceMockup = memo(function DeviceMockup({ type, width, children }: Devic
|
|
|
151
150
|
interface PreviewContentProps {
|
|
152
151
|
zoom: ZoomLevel;
|
|
153
152
|
previewTheme: PreviewTheme;
|
|
154
|
-
background: BackgroundOption;
|
|
155
153
|
children: ReactNode;
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
const PreviewContent = memo(function PreviewContent({ zoom, previewTheme,
|
|
156
|
+
const PreviewContent = memo(function PreviewContent({ zoom, previewTheme, children }: PreviewContentProps) {
|
|
159
157
|
return (
|
|
160
158
|
<div
|
|
161
159
|
data-preview-container="true"
|
|
@@ -164,7 +162,6 @@ const PreviewContent = memo(function PreviewContent({ zoom, previewTheme, backgr
|
|
|
164
162
|
width: '100%',
|
|
165
163
|
height: '100%',
|
|
166
164
|
overflow: 'auto',
|
|
167
|
-
backgroundColor: background === 'transparent' ? 'transparent' : undefined,
|
|
168
165
|
}}
|
|
169
166
|
>
|
|
170
167
|
<div
|
|
@@ -188,7 +185,6 @@ export function PreviewArea({
|
|
|
188
185
|
variant,
|
|
189
186
|
variants,
|
|
190
187
|
zoom,
|
|
191
|
-
background,
|
|
192
188
|
viewport,
|
|
193
189
|
customSize,
|
|
194
190
|
previewTheme,
|
|
@@ -212,7 +208,6 @@ export function PreviewArea({
|
|
|
212
208
|
fragmentPath={fragmentPath}
|
|
213
209
|
zoom={zoom}
|
|
214
210
|
previewTheme={previewTheme}
|
|
215
|
-
background={background}
|
|
216
211
|
useIframeIsolation={useIframeIsolation}
|
|
217
212
|
onSelectVariant={(index) => {
|
|
218
213
|
onSelectVariant(index);
|
|
@@ -230,7 +225,6 @@ export function PreviewArea({
|
|
|
230
225
|
variantName={variant.name}
|
|
231
226
|
renderContent={renderContent}
|
|
232
227
|
previewTheme={previewTheme}
|
|
233
|
-
background={background}
|
|
234
228
|
zoom={zoom}
|
|
235
229
|
useIframeIsolation={useIframeIsolation}
|
|
236
230
|
/>
|
|
@@ -240,7 +234,6 @@ export function PreviewArea({
|
|
|
240
234
|
const viewportWidth = getViewportWidth(viewport, customSize);
|
|
241
235
|
const viewportHeight = viewport === 'custom' ? customSize.height : null;
|
|
242
236
|
const isDevice = viewport === 'tablet' || viewport === 'mobile';
|
|
243
|
-
const backgroundStyle = getBackgroundStyle(background);
|
|
244
237
|
|
|
245
238
|
// Device mockup view (tablet/mobile)
|
|
246
239
|
if (isDevice && viewportWidth && variant) {
|
|
@@ -252,7 +245,7 @@ export function PreviewArea({
|
|
|
252
245
|
<div style={{ display: 'flex', gap: '16px', flex: 1 }}>
|
|
253
246
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
254
247
|
<div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)', marginBottom: '8px', textAlign: 'center' }}>Rendered</div>
|
|
255
|
-
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
248
|
+
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
256
249
|
<DeviceMockup type={viewport as 'tablet' | 'mobile'} width={viewportWidth}>
|
|
257
250
|
{useIframeIsolation ? (
|
|
258
251
|
<IsolatedPreviewFrame
|
|
@@ -264,7 +257,7 @@ export function PreviewArea({
|
|
|
264
257
|
previewKey={previewKey}
|
|
265
258
|
/>
|
|
266
259
|
) : (
|
|
267
|
-
<PreviewContent zoom={zoom} previewTheme={previewTheme}
|
|
260
|
+
<PreviewContent zoom={zoom} previewTheme={previewTheme}>
|
|
268
261
|
<ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
|
|
269
262
|
{renderContent()}
|
|
270
263
|
</ErrorBoundary>
|
|
@@ -285,7 +278,6 @@ export function PreviewArea({
|
|
|
285
278
|
borderRadius: '8px',
|
|
286
279
|
border: '1px solid var(--border)',
|
|
287
280
|
overflow: 'hidden',
|
|
288
|
-
...backgroundStyle,
|
|
289
281
|
}}
|
|
290
282
|
/>
|
|
291
283
|
</div>
|
|
@@ -307,7 +299,7 @@ export function PreviewArea({
|
|
|
307
299
|
previewKey={previewKey}
|
|
308
300
|
/>
|
|
309
301
|
) : (
|
|
310
|
-
<PreviewContent zoom={zoom} previewTheme={previewTheme}
|
|
302
|
+
<PreviewContent zoom={zoom} previewTheme={previewTheme}>
|
|
311
303
|
<ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
|
|
312
304
|
{renderContent()}
|
|
313
305
|
</ErrorBoundary>
|
|
@@ -331,7 +323,6 @@ export function PreviewArea({
|
|
|
331
323
|
borderRadius: '8px',
|
|
332
324
|
border: '1px solid var(--border)',
|
|
333
325
|
overflow: 'auto',
|
|
334
|
-
...backgroundStyle,
|
|
335
326
|
}}
|
|
336
327
|
>
|
|
337
328
|
{useIframeIsolation ? (
|
|
@@ -383,7 +374,6 @@ export function PreviewArea({
|
|
|
383
374
|
borderRadius: '8px',
|
|
384
375
|
border: '1px solid var(--border)',
|
|
385
376
|
overflow: 'hidden',
|
|
386
|
-
...backgroundStyle,
|
|
387
377
|
}}
|
|
388
378
|
/>
|
|
389
379
|
</div>
|
|
@@ -1,85 +1,19 @@
|
|
|
1
1
|
import { useEffect, useCallback } from 'react';
|
|
2
|
-
import { Button, Menu, Stack
|
|
3
|
-
import {
|
|
4
|
-
ZOOM_LEVELS,
|
|
5
|
-
type ZoomLevel,
|
|
6
|
-
type BackgroundOption,
|
|
7
|
-
} from '../constants/ui.js';
|
|
2
|
+
import { Button, Menu, Stack } from '@fragments-sdk/ui';
|
|
3
|
+
import { ZOOM_LEVELS, type ZoomLevel } from '../constants/ui.js';
|
|
8
4
|
import { ZoomIcon, ChevronDownIcon } from './Icons.js';
|
|
9
5
|
|
|
10
6
|
// Re-export types for consumers
|
|
11
|
-
export type { ZoomLevel
|
|
12
|
-
export { getBackgroundStyle } from '../constants/ui.js';
|
|
13
|
-
|
|
14
|
-
// Background options with display metadata
|
|
15
|
-
const BACKGROUND_OPTIONS_UI: { value: BackgroundOption; label: string }[] = [
|
|
16
|
-
{ value: 'white', label: 'White' },
|
|
17
|
-
{ value: 'black', label: 'Black' },
|
|
18
|
-
{ value: 'checkerboard', label: 'Checkerboard' },
|
|
19
|
-
{ value: 'transparent', label: 'Transparent' },
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
function BackgroundSwatch({ background }: { background: BackgroundOption }) {
|
|
23
|
-
const baseStyle = {
|
|
24
|
-
width: '14px',
|
|
25
|
-
height: '14px',
|
|
26
|
-
borderRadius: '4px',
|
|
27
|
-
border: '1px solid var(--border)',
|
|
28
|
-
flexShrink: 0,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
if (background === 'white') {
|
|
32
|
-
return <span aria-hidden="true" style={{ ...baseStyle, backgroundColor: '#ffffff' }} />;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (background === 'black') {
|
|
36
|
-
return <span aria-hidden="true" style={{ ...baseStyle, backgroundColor: '#171717', borderColor: '#2a2a2a' }} />;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (background === 'checkerboard') {
|
|
40
|
-
return (
|
|
41
|
-
<span
|
|
42
|
-
aria-hidden="true"
|
|
43
|
-
style={{
|
|
44
|
-
...baseStyle,
|
|
45
|
-
backgroundColor: '#ffffff',
|
|
46
|
-
backgroundImage: `
|
|
47
|
-
linear-gradient(45deg, #d4d4d8 25%, transparent 25%),
|
|
48
|
-
linear-gradient(-45deg, #d4d4d8 25%, transparent 25%),
|
|
49
|
-
linear-gradient(45deg, transparent 75%, #d4d4d8 75%),
|
|
50
|
-
linear-gradient(-45deg, transparent 75%, #d4d4d8 75%)
|
|
51
|
-
`,
|
|
52
|
-
backgroundSize: '8px 8px',
|
|
53
|
-
backgroundPosition: '0 0, 0 4px, 4px -4px, -4px 0',
|
|
54
|
-
}}
|
|
55
|
-
/>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<span
|
|
61
|
-
aria-hidden="true"
|
|
62
|
-
style={{
|
|
63
|
-
...baseStyle,
|
|
64
|
-
backgroundColor: 'transparent',
|
|
65
|
-
backgroundImage: 'linear-gradient(135deg, transparent 42%, var(--text-tertiary) 43%, var(--text-tertiary) 57%, transparent 58%)',
|
|
66
|
-
}}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
7
|
+
export type { ZoomLevel };
|
|
70
8
|
|
|
71
9
|
interface PreviewToolbarProps {
|
|
72
10
|
zoom: ZoomLevel;
|
|
73
|
-
background: BackgroundOption;
|
|
74
11
|
onZoomChange: (zoom: ZoomLevel) => void;
|
|
75
|
-
onBackgroundChange: (bg: BackgroundOption) => void;
|
|
76
12
|
}
|
|
77
13
|
|
|
78
14
|
export function PreviewToolbar({
|
|
79
15
|
zoom,
|
|
80
|
-
background,
|
|
81
16
|
onZoomChange,
|
|
82
|
-
onBackgroundChange,
|
|
83
17
|
}: PreviewToolbarProps) {
|
|
84
18
|
// Keyboard shortcuts for zoom
|
|
85
19
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
@@ -114,7 +48,6 @@ export function PreviewToolbar({
|
|
|
114
48
|
|
|
115
49
|
return (
|
|
116
50
|
<Stack direction="row" gap="sm" align="center">
|
|
117
|
-
{/* Zoom dropdown */}
|
|
118
51
|
<Menu>
|
|
119
52
|
<Menu.Trigger asChild>
|
|
120
53
|
<Button variant="ghost" size="sm" title="Zoom level (+/-/0)">
|
|
@@ -142,39 +75,6 @@ export function PreviewToolbar({
|
|
|
142
75
|
</Menu.RadioGroup>
|
|
143
76
|
</Menu.Content>
|
|
144
77
|
</Menu>
|
|
145
|
-
|
|
146
|
-
{/* Divider */}
|
|
147
|
-
<Separator orientation="vertical" />
|
|
148
|
-
|
|
149
|
-
{/* Background selector */}
|
|
150
|
-
<Menu>
|
|
151
|
-
<Menu.Trigger asChild>
|
|
152
|
-
<Button variant="ghost" size="sm" title="Background color">
|
|
153
|
-
<Stack direction="row" gap="xs" align="center">
|
|
154
|
-
<BackgroundSwatch background={background} />
|
|
155
|
-
<span>{BACKGROUND_OPTIONS_UI.find(o => o.value === background)?.label}</span>
|
|
156
|
-
<span style={{ display: 'inline-flex', width: '12px', height: '12px' }}>
|
|
157
|
-
<ChevronDownIcon />
|
|
158
|
-
</span>
|
|
159
|
-
</Stack>
|
|
160
|
-
</Button>
|
|
161
|
-
</Menu.Trigger>
|
|
162
|
-
<Menu.Content side="bottom" align="start">
|
|
163
|
-
<Menu.RadioGroup
|
|
164
|
-
value={background}
|
|
165
|
-
onValueChange={(value: string) => onBackgroundChange(value as BackgroundOption)}
|
|
166
|
-
>
|
|
167
|
-
{BACKGROUND_OPTIONS_UI.map((option) => (
|
|
168
|
-
<Menu.RadioItem key={option.value} value={option.value}>
|
|
169
|
-
<Stack direction="row" gap="sm" align="center">
|
|
170
|
-
<BackgroundSwatch background={option.value} />
|
|
171
|
-
{option.label}
|
|
172
|
-
</Stack>
|
|
173
|
-
</Menu.RadioItem>
|
|
174
|
-
))}
|
|
175
|
-
</Menu.RadioGroup>
|
|
176
|
-
</Menu.Content>
|
|
177
|
-
</Menu>
|
|
178
78
|
</Stack>
|
|
179
79
|
);
|
|
180
80
|
}
|
|
@@ -17,7 +17,6 @@ import { ErrorBoundary } from "./ErrorBoundary.js";
|
|
|
17
17
|
import { StoryRenderer, LoaderIndicator } from "./StoryRenderer.js";
|
|
18
18
|
import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
|
|
19
19
|
import { ChevronDownIcon } from "./Icons.js";
|
|
20
|
-
import { getBackgroundStyle, type BackgroundOption } from "../constants/ui.js";
|
|
21
20
|
|
|
22
21
|
interface VariantMatrixProps {
|
|
23
22
|
/** All variants to display */
|
|
@@ -30,8 +29,6 @@ interface VariantMatrixProps {
|
|
|
30
29
|
zoom: number;
|
|
31
30
|
/** Preview theme */
|
|
32
31
|
previewTheme: "light" | "dark";
|
|
33
|
-
/** Background option */
|
|
34
|
-
background: BackgroundOption;
|
|
35
32
|
/** Whether to use iframe isolation */
|
|
36
33
|
useIframeIsolation?: boolean;
|
|
37
34
|
/** Callback when a variant is clicked to focus on it */
|
|
@@ -63,7 +60,6 @@ export function VariantMatrix({
|
|
|
63
60
|
fragmentPath,
|
|
64
61
|
zoom,
|
|
65
62
|
previewTheme,
|
|
66
|
-
background,
|
|
67
63
|
useIframeIsolation = true,
|
|
68
64
|
onSelectVariant,
|
|
69
65
|
}: VariantMatrixProps) {
|
|
@@ -208,7 +204,6 @@ export function VariantMatrix({
|
|
|
208
204
|
scale={effectiveScale}
|
|
209
205
|
minHeight={gridConfig.minHeight}
|
|
210
206
|
previewTheme={previewTheme}
|
|
211
|
-
background={background}
|
|
212
207
|
useIframeIsolation={useIframeIsolation}
|
|
213
208
|
isHovered={hoveredIndex === index}
|
|
214
209
|
onHover={() => setHoveredIndex(index)}
|
|
@@ -240,7 +235,6 @@ export function VariantMatrix({
|
|
|
240
235
|
scale={effectiveScale}
|
|
241
236
|
minHeight={gridConfig.minHeight}
|
|
242
237
|
previewTheme={previewTheme}
|
|
243
|
-
background={background}
|
|
244
238
|
useIframeIsolation={useIframeIsolation}
|
|
245
239
|
isHovered={hoveredIndex === index}
|
|
246
240
|
onHover={() => setHoveredIndex(index)}
|
|
@@ -263,7 +257,6 @@ interface VariantCardProps {
|
|
|
263
257
|
scale: number;
|
|
264
258
|
minHeight: string;
|
|
265
259
|
previewTheme: "light" | "dark";
|
|
266
|
-
background: BackgroundOption;
|
|
267
260
|
useIframeIsolation: boolean;
|
|
268
261
|
isHovered: boolean;
|
|
269
262
|
onHover: () => void;
|
|
@@ -279,15 +272,12 @@ function VariantCard({
|
|
|
279
272
|
scale,
|
|
280
273
|
minHeight,
|
|
281
274
|
previewTheme,
|
|
282
|
-
background,
|
|
283
275
|
useIframeIsolation,
|
|
284
276
|
isHovered,
|
|
285
277
|
onHover,
|
|
286
278
|
onLeave,
|
|
287
279
|
onClick,
|
|
288
280
|
}: VariantCardProps) {
|
|
289
|
-
const backgroundStyle = getBackgroundStyle(background);
|
|
290
|
-
|
|
291
281
|
return (
|
|
292
282
|
<div
|
|
293
283
|
style={{
|
|
@@ -373,7 +363,6 @@ function VariantCard({
|
|
|
373
363
|
display: 'flex',
|
|
374
364
|
alignItems: 'center',
|
|
375
365
|
justifyContent: 'center',
|
|
376
|
-
...backgroundStyle,
|
|
377
366
|
}}
|
|
378
367
|
>
|
|
379
368
|
{useIframeIsolation ? (
|
|
@@ -102,12 +102,6 @@ export function getRelationshipConfig(relationship: string) {
|
|
|
102
102
|
return RELATIONSHIP_CONFIG[relationship as RelationshipType] || RELATIONSHIP_CONFIG.related;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* Preview background options.
|
|
107
|
-
*/
|
|
108
|
-
export const BACKGROUND_OPTIONS = ['white', 'black', 'checkerboard', 'transparent'] as const;
|
|
109
|
-
export type BackgroundOption = (typeof BACKGROUND_OPTIONS)[number];
|
|
110
|
-
|
|
111
105
|
/**
|
|
112
106
|
* Zoom level options.
|
|
113
107
|
*/
|
|
@@ -140,34 +134,6 @@ export function getViewportWidth(viewport: ViewportPreset, customSize: ViewportS
|
|
|
140
134
|
return VIEWPORT_PRESETS[viewport]?.width ?? null;
|
|
141
135
|
}
|
|
142
136
|
|
|
143
|
-
/**
|
|
144
|
-
* Get CSS background style for preview pane.
|
|
145
|
-
*/
|
|
146
|
-
export function getBackgroundStyle(bg: BackgroundOption): React.CSSProperties {
|
|
147
|
-
switch (bg) {
|
|
148
|
-
case 'white':
|
|
149
|
-
return { backgroundColor: '#ffffff' };
|
|
150
|
-
case 'black':
|
|
151
|
-
return { backgroundColor: '#000000' };
|
|
152
|
-
case 'transparent':
|
|
153
|
-
return { backgroundColor: 'transparent' };
|
|
154
|
-
case 'checkerboard':
|
|
155
|
-
return {
|
|
156
|
-
backgroundColor: '#ffffff',
|
|
157
|
-
backgroundImage: `
|
|
158
|
-
linear-gradient(45deg, #e5e5e5 25%, transparent 25%),
|
|
159
|
-
linear-gradient(-45deg, #e5e5e5 25%, transparent 25%),
|
|
160
|
-
linear-gradient(45deg, transparent 75%, #e5e5e5 75%),
|
|
161
|
-
linear-gradient(-45deg, transparent 75%, #e5e5e5 75%)
|
|
162
|
-
`,
|
|
163
|
-
backgroundSize: '16px 16px',
|
|
164
|
-
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
|
165
|
-
};
|
|
166
|
-
default:
|
|
167
|
-
return { backgroundColor: '#ffffff' };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
137
|
/**
|
|
172
138
|
* Storage keys for localStorage persistence.
|
|
173
139
|
*/
|
|
@@ -262,7 +262,7 @@ export const SHORTCUTS = [
|
|
|
262
262
|
{ keys: ["p"], description: "Toggle addons panel" },
|
|
263
263
|
{ keys: ["m"], description: "Matrix view" },
|
|
264
264
|
{ keys: ["v"], description: "Responsive view" },
|
|
265
|
-
{ keys: ["/", "⌘K"], description: "
|
|
265
|
+
{ keys: ["/", "⌘K"], description: "Command palette" },
|
|
266
266
|
{ keys: ["?"], description: "Show shortcuts" },
|
|
267
267
|
{ keys: ["Esc"], description: "Close / Clear" },
|
|
268
268
|
];
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* - Component path
|
|
6
6
|
* - Variant index/name
|
|
7
7
|
* - Edited props
|
|
8
|
-
* - View settings (zoom,
|
|
8
|
+
* - View settings (zoom, viewport)
|
|
9
9
|
*
|
|
10
|
-
* URL format: #/ComponentName/VariantName?props=base64&zoom=100
|
|
10
|
+
* URL format: #/ComponentName/VariantName?props=base64&zoom=100
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
@@ -21,7 +21,6 @@ export interface UrlState {
|
|
|
21
21
|
props: Record<string, unknown>;
|
|
22
22
|
/** View settings */
|
|
23
23
|
zoom: number;
|
|
24
|
-
background: string;
|
|
25
24
|
viewport: string;
|
|
26
25
|
customWidth: number | null;
|
|
27
26
|
customHeight: number | null;
|
|
@@ -32,7 +31,6 @@ const DEFAULT_STATE: UrlState = {
|
|
|
32
31
|
variant: null,
|
|
33
32
|
props: {},
|
|
34
33
|
zoom: 100,
|
|
35
|
-
background: "transparent",
|
|
36
34
|
viewport: "responsive",
|
|
37
35
|
customWidth: null,
|
|
38
36
|
customHeight: null,
|
|
@@ -99,11 +97,6 @@ function parseUrl(): UrlState {
|
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
const bg = params.get("bg");
|
|
103
|
-
if (bg && ["white", "black", "checkerboard", "transparent"].includes(bg)) {
|
|
104
|
-
state.background = bg;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
100
|
const viewport = params.get("viewport");
|
|
108
101
|
if (viewport && ["responsive", "desktop", "tablet", "mobile", "custom"].includes(viewport)) {
|
|
109
102
|
state.viewport = viewport;
|
|
@@ -155,10 +148,6 @@ function buildUrl(state: Partial<UrlState>): string {
|
|
|
155
148
|
params.set("zoom", String(state.zoom));
|
|
156
149
|
}
|
|
157
150
|
|
|
158
|
-
if (state.background && state.background !== "transparent") {
|
|
159
|
-
params.set("bg", state.background);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
151
|
if (state.viewport && state.viewport !== "responsive") {
|
|
163
152
|
params.set("viewport", state.viewport);
|
|
164
153
|
}
|
|
@@ -249,7 +238,6 @@ export function useUrlState() {
|
|
|
249
238
|
const setViewSettings = useCallback(
|
|
250
239
|
(settings: {
|
|
251
240
|
zoom?: number;
|
|
252
|
-
background?: string;
|
|
253
241
|
viewport?: string;
|
|
254
242
|
customWidth?: number | null;
|
|
255
243
|
customHeight?: number | null;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* View settings state management.
|
|
3
|
-
* Handles zoom,
|
|
3
|
+
* Handles zoom, viewport, and theme settings with URL sync.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useReducer, useCallback, useMemo } from 'react';
|
|
7
|
-
import type { ZoomLevel,
|
|
7
|
+
import type { ZoomLevel, ViewportPreset, ViewportSize } from '../constants/ui.js';
|
|
8
8
|
|
|
9
9
|
// Preview theme type (for canvas theming)
|
|
10
10
|
export type PreviewTheme = 'light' | 'dark';
|
|
11
11
|
|
|
12
12
|
interface ViewSettings {
|
|
13
13
|
zoom: ZoomLevel;
|
|
14
|
-
background: BackgroundOption;
|
|
15
14
|
viewport: ViewportPreset;
|
|
16
15
|
customSize: ViewportSize;
|
|
17
16
|
previewTheme: PreviewTheme;
|
|
@@ -19,7 +18,6 @@ interface ViewSettings {
|
|
|
19
18
|
|
|
20
19
|
type ViewSettingsAction =
|
|
21
20
|
| { type: 'SET_ZOOM'; payload: ZoomLevel }
|
|
22
|
-
| { type: 'SET_BACKGROUND'; payload: BackgroundOption }
|
|
23
21
|
| { type: 'SET_VIEWPORT'; payload: ViewportPreset }
|
|
24
22
|
| { type: 'SET_CUSTOM_SIZE'; payload: ViewportSize }
|
|
25
23
|
| { type: 'SET_PREVIEW_THEME'; payload: PreviewTheme }
|
|
@@ -28,7 +26,6 @@ type ViewSettingsAction =
|
|
|
28
26
|
|
|
29
27
|
interface ViewSettingsInit {
|
|
30
28
|
zoom?: ZoomLevel;
|
|
31
|
-
background?: BackgroundOption;
|
|
32
29
|
viewport?: ViewportPreset;
|
|
33
30
|
customSize?: ViewportSize;
|
|
34
31
|
previewTheme?: PreviewTheme;
|
|
@@ -37,7 +34,6 @@ interface ViewSettingsInit {
|
|
|
37
34
|
function createInitialState(init?: ViewSettingsInit): ViewSettings {
|
|
38
35
|
return {
|
|
39
36
|
zoom: init?.zoom ?? 100,
|
|
40
|
-
background: init?.background ?? 'transparent',
|
|
41
37
|
viewport: init?.viewport ?? 'responsive',
|
|
42
38
|
customSize: init?.customSize ?? { width: null, height: null },
|
|
43
39
|
previewTheme: init?.previewTheme ?? 'light',
|
|
@@ -48,8 +44,6 @@ function viewSettingsReducer(state: ViewSettings, action: ViewSettingsAction): V
|
|
|
48
44
|
switch (action.type) {
|
|
49
45
|
case 'SET_ZOOM':
|
|
50
46
|
return { ...state, zoom: action.payload };
|
|
51
|
-
case 'SET_BACKGROUND':
|
|
52
|
-
return { ...state, background: action.payload };
|
|
53
47
|
case 'SET_VIEWPORT':
|
|
54
48
|
return { ...state, viewport: action.payload };
|
|
55
49
|
case 'SET_CUSTOM_SIZE':
|
|
@@ -68,12 +62,11 @@ function viewSettingsReducer(state: ViewSettings, action: ViewSettingsAction): V
|
|
|
68
62
|
interface UseViewSettingsOptions {
|
|
69
63
|
initialState?: ViewSettingsInit;
|
|
70
64
|
onZoomChange?: (zoom: ZoomLevel) => void;
|
|
71
|
-
onBackgroundChange?: (bg: BackgroundOption) => void;
|
|
72
65
|
onViewportChange?: (viewport: ViewportPreset, customSize?: ViewportSize) => void;
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
export function useViewSettings(options: UseViewSettingsOptions = {}) {
|
|
76
|
-
const { initialState: init, onZoomChange,
|
|
69
|
+
const { initialState: init, onZoomChange, onViewportChange } = options;
|
|
77
70
|
|
|
78
71
|
const [state, dispatch] = useReducer(
|
|
79
72
|
viewSettingsReducer,
|
|
@@ -86,11 +79,6 @@ export function useViewSettings(options: UseViewSettingsOptions = {}) {
|
|
|
86
79
|
onZoomChange?.(zoom);
|
|
87
80
|
}, [onZoomChange]);
|
|
88
81
|
|
|
89
|
-
const setBackground = useCallback((bg: BackgroundOption) => {
|
|
90
|
-
dispatch({ type: 'SET_BACKGROUND', payload: bg });
|
|
91
|
-
onBackgroundChange?.(bg);
|
|
92
|
-
}, [onBackgroundChange]);
|
|
93
|
-
|
|
94
82
|
const setViewport = useCallback((viewport: ViewportPreset) => {
|
|
95
83
|
dispatch({ type: 'SET_VIEWPORT', payload: viewport });
|
|
96
84
|
onViewportChange?.(viewport);
|
|
@@ -111,12 +99,11 @@ export function useViewSettings(options: UseViewSettingsOptions = {}) {
|
|
|
111
99
|
|
|
112
100
|
const actions = useMemo(() => ({
|
|
113
101
|
setZoom,
|
|
114
|
-
setBackground,
|
|
115
102
|
setViewport,
|
|
116
103
|
setCustomSize,
|
|
117
104
|
setPreviewTheme,
|
|
118
105
|
toggleTheme,
|
|
119
|
-
}), [setZoom,
|
|
106
|
+
}), [setZoom, setViewport, setCustomSize, setPreviewTheme, toggleTheme]);
|
|
120
107
|
|
|
121
108
|
return { ...state, ...actions };
|
|
122
109
|
}
|