@fragments-sdk/cli 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
13
|
-
import { Button, Stack, Text, Badge,
|
|
13
|
+
import { Button, Stack, Text, Badge, CodeBlock, Alert, Box } from "@fragments-sdk/ui";
|
|
14
14
|
import type { PlayFunction, PlayFunctionContext, FragmentVariant } from "../../core/index.js";
|
|
15
|
+
import { Play } from "@phosphor-icons/react";
|
|
15
16
|
import {
|
|
16
17
|
PlayIcon,
|
|
17
18
|
CheckIcon,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
BreakpointIcon,
|
|
28
29
|
BreakpointEmptyIcon,
|
|
29
30
|
} from "./Icons.js";
|
|
31
|
+
import { PanelShell } from "./PanelShell.js";
|
|
30
32
|
|
|
31
33
|
// Step execution state
|
|
32
34
|
interface StepResult {
|
|
@@ -424,27 +426,17 @@ export function InteractionsPanel({
|
|
|
424
426
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
425
427
|
};
|
|
426
428
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
<EmptyState>
|
|
439
|
-
<EmptyState.Icon>
|
|
440
|
-
<PlayIcon style={{ width: 24, height: 24 }} />
|
|
441
|
-
</EmptyState.Icon>
|
|
442
|
-
<EmptyState.Title>No interactions defined</EmptyState.Title>
|
|
443
|
-
<EmptyState.Description>
|
|
444
|
-
This variant doesn't have a play function. Add a <code style={{ padding: '2px 4px', background: 'var(--bg-secondary)', borderRadius: '4px', fontSize: '12px' }}>play</code> function to your Storybook story to enable interaction testing.
|
|
445
|
-
</EmptyState.Description>
|
|
446
|
-
<div style={{ marginTop: '16px', width: '100%' }}>
|
|
447
|
-
<CodeBlock language="typescript">{`export const Default = {
|
|
429
|
+
// Empty state for no play function
|
|
430
|
+
const emptyConfig = !hasPlayFunction ? {
|
|
431
|
+
icon: <Play size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
|
|
432
|
+
title: "No interactions defined",
|
|
433
|
+
description: (
|
|
434
|
+
<>
|
|
435
|
+
This variant doesn't have a play function. Add a <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>play</Box> function to your Storybook story to enable interaction testing.
|
|
436
|
+
</>
|
|
437
|
+
),
|
|
438
|
+
action: (
|
|
439
|
+
<CodeBlock language="typescript">{`export const Default = {
|
|
448
440
|
play: async ({ canvasElement, step }) => {
|
|
449
441
|
const canvas = within(canvasElement);
|
|
450
442
|
|
|
@@ -459,110 +451,102 @@ export function InteractionsPanel({
|
|
|
459
451
|
).toBeInTheDocument();
|
|
460
452
|
}
|
|
461
453
|
};`}</CodeBlock>
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
454
|
+
),
|
|
455
|
+
} : undefined;
|
|
456
|
+
|
|
457
|
+
// Toolbar with duration, debug controls, and run button
|
|
458
|
+
const toolbar = hasPlayFunction ? (
|
|
459
|
+
<Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
|
|
460
|
+
<Stack direction="row" align="center" gap="sm">
|
|
461
|
+
{result.duration !== undefined && (
|
|
462
|
+
<Text size="xs" color="tertiary">{formatDuration(result.duration)}</Text>
|
|
463
|
+
)}
|
|
464
|
+
</Stack>
|
|
465
|
+
<Stack direction="row" align="center" gap="sm">
|
|
466
|
+
{/* Debug mode toggle */}
|
|
467
|
+
<Button
|
|
468
|
+
onClick={toggleDebugMode}
|
|
469
|
+
variant="ghost"
|
|
470
|
+
size="sm"
|
|
471
|
+
title={debugState.mode === 'debug' ? "Exit debug mode" : "Enable debug mode (F5 to run with debugger)"}
|
|
472
|
+
style={{
|
|
473
|
+
color: debugState.mode === 'debug' ? '#ea580c' : undefined,
|
|
474
|
+
background: debugState.mode === 'debug' ? 'color-mix(in srgb, #f97316 10%, transparent)' : undefined,
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<BugIcon style={{ width: 16, height: 16 }} />
|
|
478
|
+
</Button>
|
|
479
|
+
|
|
480
|
+
{/* Debug controls when paused */}
|
|
481
|
+
{debugState.isPaused && (
|
|
482
|
+
<>
|
|
484
483
|
<Button
|
|
485
|
-
onClick={
|
|
484
|
+
onClick={handleResume}
|
|
486
485
|
variant="ghost"
|
|
487
486
|
size="sm"
|
|
488
|
-
title=
|
|
489
|
-
style={{
|
|
490
|
-
color: debugState.mode === 'debug' ? '#ea580c' : undefined,
|
|
491
|
-
background: debugState.mode === 'debug' ? 'color-mix(in srgb, #f97316 10%, transparent)' : undefined,
|
|
492
|
-
}}
|
|
487
|
+
title="Continue (F8)"
|
|
488
|
+
style={{ color: '#16a34a' }}
|
|
493
489
|
>
|
|
494
|
-
<
|
|
490
|
+
<ContinueIcon style={{ width: 16, height: 16 }} />
|
|
495
491
|
</Button>
|
|
496
|
-
|
|
497
|
-
{/* Debug controls when paused */}
|
|
498
|
-
{debugState.isPaused && (
|
|
499
|
-
<>
|
|
500
|
-
<Button
|
|
501
|
-
onClick={handleResume}
|
|
502
|
-
variant="ghost"
|
|
503
|
-
size="sm"
|
|
504
|
-
title="Continue (F8)"
|
|
505
|
-
style={{ color: '#16a34a' }}
|
|
506
|
-
>
|
|
507
|
-
<ContinueIcon style={{ width: 16, height: 16 }} />
|
|
508
|
-
</Button>
|
|
509
|
-
<Button
|
|
510
|
-
onClick={handleStepOver}
|
|
511
|
-
variant="ghost"
|
|
512
|
-
size="sm"
|
|
513
|
-
title="Step over (F10)"
|
|
514
|
-
style={{ color: '#2563eb' }}
|
|
515
|
-
>
|
|
516
|
-
<StepOverIcon style={{ width: 16, height: 16 }} />
|
|
517
|
-
</Button>
|
|
518
|
-
</>
|
|
519
|
-
)}
|
|
520
|
-
|
|
521
492
|
<Button
|
|
522
|
-
onClick={
|
|
523
|
-
|
|
524
|
-
variant={result.status === "running" || result.status === "paused" ? "outline" : "solid"}
|
|
493
|
+
onClick={handleStepOver}
|
|
494
|
+
variant="ghost"
|
|
525
495
|
size="sm"
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
? { background: 'var(--bg-secondary)', color: 'var(--text-muted)', cursor: 'not-allowed' }
|
|
529
|
-
: { background: 'var(--color-accent)', color: '#fff' }
|
|
530
|
-
}
|
|
496
|
+
title="Step over (F10)"
|
|
497
|
+
style={{ color: '#2563eb' }}
|
|
531
498
|
>
|
|
532
|
-
{
|
|
533
|
-
<>
|
|
534
|
-
<LoadingIcon style={{ width: 16, height: 16, animation: 'spin 1s linear infinite' }} />
|
|
535
|
-
Running...
|
|
536
|
-
</>
|
|
537
|
-
) : result.status === "paused" ? (
|
|
538
|
-
<>
|
|
539
|
-
<PauseIcon style={{ width: 16, height: 16 }} />
|
|
540
|
-
Paused
|
|
541
|
-
</>
|
|
542
|
-
) : result.status === "idle" ? (
|
|
543
|
-
<>
|
|
544
|
-
<PlayIcon style={{ width: 16, height: 16 }} />
|
|
545
|
-
{debugState.mode === 'debug' ? 'Debug' : 'Run'}
|
|
546
|
-
</>
|
|
547
|
-
) : (
|
|
548
|
-
<>
|
|
549
|
-
<RefreshIcon style={{ width: 16, height: 16 }} />
|
|
550
|
-
Rerun
|
|
551
|
-
</>
|
|
552
|
-
)}
|
|
499
|
+
<StepOverIcon style={{ width: 16, height: 16 }} />
|
|
553
500
|
</Button>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
501
|
+
</>
|
|
502
|
+
)}
|
|
503
|
+
|
|
504
|
+
<Button
|
|
505
|
+
onClick={() => runInteractions(debugState.mode === 'debug')}
|
|
506
|
+
disabled={result.status === "running"}
|
|
507
|
+
variant={result.status === "running" || result.status === "paused" ? "outline" : "solid"}
|
|
508
|
+
size="sm"
|
|
509
|
+
style={
|
|
510
|
+
result.status === "running" || result.status === "paused"
|
|
511
|
+
? { background: 'var(--bg-secondary)', color: 'var(--text-muted)', cursor: 'not-allowed' }
|
|
512
|
+
: { background: 'var(--color-accent)', color: '#fff' }
|
|
513
|
+
}
|
|
514
|
+
>
|
|
515
|
+
{result.status === "running" ? (
|
|
516
|
+
<>
|
|
517
|
+
<LoadingIcon style={{ width: 16, height: 16, animation: 'spin 1s linear infinite' }} />
|
|
518
|
+
Running...
|
|
519
|
+
</>
|
|
520
|
+
) : result.status === "paused" ? (
|
|
521
|
+
<>
|
|
522
|
+
<PauseIcon style={{ width: 16, height: 16 }} />
|
|
523
|
+
Paused
|
|
524
|
+
</>
|
|
525
|
+
) : result.status === "idle" ? (
|
|
526
|
+
<>
|
|
527
|
+
<PlayIcon style={{ width: 16, height: 16 }} />
|
|
528
|
+
{debugState.mode === 'debug' ? 'Debug' : 'Run'}
|
|
529
|
+
</>
|
|
530
|
+
) : (
|
|
531
|
+
<>
|
|
532
|
+
<RefreshIcon style={{ width: 16, height: 16 }} />
|
|
533
|
+
Rerun
|
|
534
|
+
</>
|
|
535
|
+
)}
|
|
536
|
+
</Button>
|
|
537
|
+
</Stack>
|
|
538
|
+
</Stack>
|
|
539
|
+
) : undefined;
|
|
557
540
|
|
|
558
|
-
|
|
559
|
-
|
|
541
|
+
return (
|
|
542
|
+
<div data-interactions-panel style={{ height: '100%' }}>
|
|
543
|
+
<PanelShell toolbar={toolbar} empty={emptyConfig} bodyPadding="none">
|
|
560
544
|
{result.status === "idle" ? (
|
|
561
|
-
<
|
|
545
|
+
<Box padding="lg" style={{ textAlign: 'center' }}>
|
|
562
546
|
<Text size="sm" color="tertiary">
|
|
563
547
|
Click "Run" to execute the interaction tests
|
|
564
548
|
</Text>
|
|
565
|
-
</
|
|
549
|
+
</Box>
|
|
566
550
|
) : (
|
|
567
551
|
<Stack direction="column" gap="sm" style={{ padding: '16px' }}>
|
|
568
552
|
{/* Overall status */}
|
|
@@ -743,7 +727,7 @@ export function InteractionsPanel({
|
|
|
743
727
|
|
|
744
728
|
{/* Keyboard shortcuts help in debug mode */}
|
|
745
729
|
{debugState.mode === 'debug' && (
|
|
746
|
-
<
|
|
730
|
+
<Box padding="sm" background="secondary" rounded="lg" style={{ marginTop: '16px' }}>
|
|
747
731
|
<Text size="xs" weight="medium" color="tertiary" style={{ marginBottom: '4px' }}>Keyboard shortcuts:</Text>
|
|
748
732
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
|
749
733
|
<Text size="xs" color="tertiary"><Badge size="sm" variant="default">F5</Badge> Run with debugger</Text>
|
|
@@ -751,23 +735,23 @@ export function InteractionsPanel({
|
|
|
751
735
|
<Text size="xs" color="tertiary"><Badge size="sm" variant="default">F9</Badge> Toggle breakpoint</Text>
|
|
752
736
|
<Text size="xs" color="tertiary"><Badge size="sm" variant="default">F10</Badge> Step over</Text>
|
|
753
737
|
</div>
|
|
754
|
-
</
|
|
738
|
+
</Box>
|
|
755
739
|
)}
|
|
756
740
|
|
|
757
741
|
{/* Top-level error (if no steps failed) */}
|
|
758
742
|
{result.error && !result.steps.some((s) => s.status === "failed") && (
|
|
759
|
-
<
|
|
743
|
+
<Box style={{ marginTop: '16px' }}>
|
|
760
744
|
<Text size="xs" weight="medium" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '8px' }}>
|
|
761
745
|
Error
|
|
762
746
|
</Text>
|
|
763
747
|
<pre style={{ fontSize: '12px', color: '#dc2626', background: 'color-mix(in srgb, #ef4444 10%, transparent)', padding: '12px', borderRadius: '4px', overflowX: 'auto', whiteSpace: 'pre-wrap' }}>
|
|
764
748
|
{result.error}
|
|
765
749
|
</pre>
|
|
766
|
-
</
|
|
750
|
+
</Box>
|
|
767
751
|
)}
|
|
768
752
|
</Stack>
|
|
769
753
|
)}
|
|
770
|
-
</
|
|
754
|
+
</PanelShell>
|
|
771
755
|
</div>
|
|
772
756
|
);
|
|
773
757
|
}
|
|
@@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { useState, useRef, useEffect } from 'react';
|
|
5
5
|
import { HexColorPicker } from 'react-colorful';
|
|
6
|
-
import { Button, Input, Toggle, Stack, Text, Card, Field, Separator, Select } from '@fragments-sdk/ui';
|
|
6
|
+
import { Box, Button, Input, Toggle, Stack, Text, Card, Field, Separator, Select } from '@fragments-sdk/ui';
|
|
7
7
|
import { generateColorScheme, type ColorSchemeType } from '../utils/colorSchemes.js';
|
|
8
8
|
import { ChevronDownIcon } from './Icons.js';
|
|
9
9
|
|
|
@@ -106,7 +106,7 @@ export function LandingPage({ onSubmit }: LandingPageProps) {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
return (
|
|
109
|
-
<
|
|
109
|
+
<Box background="primary" style={{ minHeight: '100vh' }}>
|
|
110
110
|
<div style={{ maxWidth: '672px', margin: '0 auto', padding: '64px 24px' }}>
|
|
111
111
|
{/* Header */}
|
|
112
112
|
<Stack direction="column" gap="sm" style={{ marginBottom: '48px' }}>
|
|
@@ -416,6 +416,6 @@ export function LandingPage({ onSubmit }: LandingPageProps) {
|
|
|
416
416
|
</Stack>
|
|
417
417
|
</form>
|
|
418
418
|
</div>
|
|
419
|
-
</
|
|
419
|
+
</Box>
|
|
420
420
|
);
|
|
421
421
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import type { FragmentDefinition } from '../../core/index.js';
|
|
3
|
-
import { Sidebar, useSidebar, Text, Menu } from '@fragments-sdk/ui';
|
|
3
|
+
import { Sidebar, useSidebar, Text, Menu, Button, Box, Stack } from '@fragments-sdk/ui';
|
|
4
4
|
|
|
5
5
|
// Fuzzy matching utility
|
|
6
6
|
interface FuzzyMatch {
|
|
@@ -160,6 +160,32 @@ const FilterIcon = ({ active }: { active?: boolean }) => (
|
|
|
160
160
|
|
|
161
161
|
const isInstalled = (path: string) => path.startsWith('@');
|
|
162
162
|
|
|
163
|
+
/** Normalize category names to Title Case: "form" → "Form", "Data Display" stays */
|
|
164
|
+
function titleCase(str: string): string {
|
|
165
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Normalize category to a canonical key so "Form" and "Forms" merge into one group.
|
|
169
|
+
* Returns the singular form as grouping key. */
|
|
170
|
+
function categoryKey(raw: string): string {
|
|
171
|
+
const tc = titleCase(raw.trim());
|
|
172
|
+
// Strip trailing "s" plurals (Forms→Form, Buttons→Button) but not short words like "Tabs"
|
|
173
|
+
if (tc.length > 4 && tc.endsWith('s') && !tc.endsWith('ss')) {
|
|
174
|
+
return tc.slice(0, -1);
|
|
175
|
+
}
|
|
176
|
+
return tc;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Pluralize a category name for display: "Form" → "Forms", "General" stays */
|
|
180
|
+
function pluralizeCategory(key: string): string {
|
|
181
|
+
// Already plural or naturally non-plural categories
|
|
182
|
+
const nonPlural = new Set(['General', 'Feedback', 'Layout', 'Navigation', 'Typography', 'Deprecated', 'Data Display']);
|
|
183
|
+
if (nonPlural.has(key)) return key;
|
|
184
|
+
// Already ends in s (edge case)
|
|
185
|
+
if (key.endsWith('s')) return key;
|
|
186
|
+
return key + 's';
|
|
187
|
+
}
|
|
188
|
+
|
|
163
189
|
export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect, showHealth, onHealthClick }: LeftSidebarProps) {
|
|
164
190
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
165
191
|
const [showCustom, setShowCustom] = useState(true);
|
|
@@ -229,6 +255,22 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
|
|
|
229
255
|
);
|
|
230
256
|
}, [sourceFiltered, searchResults, isFilterActive, showLibrary]);
|
|
231
257
|
|
|
258
|
+
// Group items by category when not searching (for large lists)
|
|
259
|
+
const categoryGroups = useMemo(() => {
|
|
260
|
+
if (searchResults || flatItems.length < 30) return null; // Flat list for small sets or search
|
|
261
|
+
const groups = new Map<string, typeof flatItems>();
|
|
262
|
+
for (const item of flatItems) {
|
|
263
|
+
const raw = item.fragment.meta.category || 'Uncategorized';
|
|
264
|
+
const key = categoryKey(raw);
|
|
265
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
266
|
+
groups.get(key)!.push(item);
|
|
267
|
+
}
|
|
268
|
+
// Sort categories alphabetically, map key → display label
|
|
269
|
+
return [...groups.entries()]
|
|
270
|
+
.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()))
|
|
271
|
+
.map(([key, items]) => [pluralizeCategory(key), items] as const);
|
|
272
|
+
}, [flatItems, searchResults]);
|
|
273
|
+
|
|
232
274
|
const keyboardItems = useMemo(() => {
|
|
233
275
|
const componentItems = flatItems.map((item) => ({ type: 'component' as const, path: item.path }));
|
|
234
276
|
if (!onHealthClick) return componentItems;
|
|
@@ -245,6 +287,19 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
|
|
|
245
287
|
}
|
|
246
288
|
}, [focusedIndex, keyboardItems.length]);
|
|
247
289
|
|
|
290
|
+
// Scroll the active sidebar item into view on mount and when activeFragment changes
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!activeFragment || !navRef.current) return;
|
|
293
|
+
// Small delay to let the DOM render after category grouping
|
|
294
|
+
const timer = setTimeout(() => {
|
|
295
|
+
const activeButton = navRef.current?.querySelector<HTMLButtonElement>('[aria-current="page"]');
|
|
296
|
+
if (activeButton) {
|
|
297
|
+
activeButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
298
|
+
}
|
|
299
|
+
}, 100);
|
|
300
|
+
return () => clearTimeout(timer);
|
|
301
|
+
}, [activeFragment]);
|
|
302
|
+
|
|
248
303
|
useEffect(() => {
|
|
249
304
|
setFocusedIndex(-1);
|
|
250
305
|
}, [searchQuery]);
|
|
@@ -312,82 +367,105 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
|
|
|
312
367
|
</Sidebar.Section>
|
|
313
368
|
)}
|
|
314
369
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
padding: '2px',
|
|
328
|
-
display: 'flex',
|
|
329
|
-
alignItems: 'center',
|
|
330
|
-
color: 'var(--text-tertiary)',
|
|
331
|
-
borderRadius: '4px',
|
|
332
|
-
}}
|
|
333
|
-
>
|
|
334
|
-
<FilterIcon active={isFilterActive} />
|
|
335
|
-
</button>
|
|
336
|
-
</Menu.Trigger>
|
|
337
|
-
<Menu.Content>
|
|
338
|
-
<Menu.CheckboxItem checked={showCustom} onCheckedChange={setShowCustom}>
|
|
339
|
-
Custom
|
|
340
|
-
</Menu.CheckboxItem>
|
|
341
|
-
<Menu.CheckboxItem checked={showLibrary} onCheckedChange={setShowLibrary}>
|
|
342
|
-
Fragments UI
|
|
343
|
-
</Menu.CheckboxItem>
|
|
344
|
-
</Menu.Content>
|
|
345
|
-
</Menu>
|
|
346
|
-
) : undefined}
|
|
347
|
-
>
|
|
348
|
-
{flatItems.map((item) => {
|
|
349
|
-
const hasError = !!(item.fragment as any)?._loadError;
|
|
350
|
-
return (
|
|
351
|
-
<Sidebar.Item
|
|
352
|
-
key={item.path}
|
|
353
|
-
active={activeFragment === item.path}
|
|
354
|
-
onClick={() => handleSelect(item.path)}
|
|
355
|
-
>
|
|
356
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}>
|
|
357
|
-
<HighlightedText
|
|
358
|
-
text={item.fragment.meta.name}
|
|
359
|
-
indices={highlightMap.get(item.path) || []}
|
|
360
|
-
/>
|
|
361
|
-
{hasError && (
|
|
362
|
-
<span
|
|
363
|
-
title="Missing dependencies"
|
|
364
|
-
style={{ color: 'var(--color-warning, #f59e0b)', fontSize: '14px', lineHeight: 1, flexShrink: 0 }}
|
|
370
|
+
{categoryGroups ? (
|
|
371
|
+
// Grouped by category for large component lists
|
|
372
|
+
<>
|
|
373
|
+
{categoryGroups.map(([category, items]) => (
|
|
374
|
+
<Sidebar.Section key={category} label={`${category} (${items.length})`}>
|
|
375
|
+
{items.map((item) => {
|
|
376
|
+
const hasError = !!(item.fragment as any)?._loadError;
|
|
377
|
+
return (
|
|
378
|
+
<Sidebar.Item
|
|
379
|
+
key={item.path}
|
|
380
|
+
active={activeFragment === item.path}
|
|
381
|
+
onClick={() => handleSelect(item.path)}
|
|
365
382
|
>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
383
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}>
|
|
384
|
+
<HighlightedText
|
|
385
|
+
text={item.fragment.meta.name}
|
|
386
|
+
indices={highlightMap.get(item.path) || []}
|
|
387
|
+
/>
|
|
388
|
+
{hasError && (
|
|
389
|
+
<span
|
|
390
|
+
title="Missing dependencies"
|
|
391
|
+
style={{ color: 'var(--color-warning, #f59e0b)', fontSize: '14px', lineHeight: 1, flexShrink: 0 }}
|
|
392
|
+
>
|
|
393
|
+
⚠
|
|
394
|
+
</span>
|
|
395
|
+
)}
|
|
396
|
+
</span>
|
|
397
|
+
</Sidebar.Item>
|
|
398
|
+
);
|
|
399
|
+
})}
|
|
400
|
+
</Sidebar.Section>
|
|
401
|
+
))}
|
|
402
|
+
</>
|
|
403
|
+
) : (
|
|
404
|
+
// Flat list for search results or small component sets
|
|
405
|
+
<Sidebar.Section
|
|
406
|
+
label="Components"
|
|
407
|
+
action={hasBothSources ? (
|
|
408
|
+
<Menu modal={false}>
|
|
409
|
+
<Menu.Trigger asChild>
|
|
410
|
+
<Button variant="ghost" size="sm" icon aria-label="Filter components by source">
|
|
411
|
+
<FilterIcon active={isFilterActive} />
|
|
412
|
+
</Button>
|
|
413
|
+
</Menu.Trigger>
|
|
414
|
+
<Menu.Content>
|
|
415
|
+
<Menu.CheckboxItem checked={showCustom} onCheckedChange={setShowCustom}>
|
|
416
|
+
Custom
|
|
417
|
+
</Menu.CheckboxItem>
|
|
418
|
+
<Menu.CheckboxItem checked={showLibrary} onCheckedChange={setShowLibrary}>
|
|
419
|
+
Fragments UI
|
|
420
|
+
</Menu.CheckboxItem>
|
|
421
|
+
</Menu.Content>
|
|
422
|
+
</Menu>
|
|
423
|
+
) : undefined}
|
|
424
|
+
>
|
|
425
|
+
{flatItems.map((item) => {
|
|
426
|
+
const hasError = !!(item.fragment as any)?._loadError;
|
|
427
|
+
return (
|
|
428
|
+
<Sidebar.Item
|
|
429
|
+
key={item.path}
|
|
430
|
+
active={activeFragment === item.path}
|
|
431
|
+
onClick={() => handleSelect(item.path)}
|
|
432
|
+
>
|
|
433
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}>
|
|
434
|
+
<HighlightedText
|
|
435
|
+
text={item.fragment.meta.name}
|
|
436
|
+
indices={highlightMap.get(item.path) || []}
|
|
437
|
+
/>
|
|
438
|
+
{hasError && (
|
|
439
|
+
<span
|
|
440
|
+
title="Missing dependencies"
|
|
441
|
+
style={{ color: 'var(--color-warning, #f59e0b)', fontSize: '14px', lineHeight: 1, flexShrink: 0 }}
|
|
442
|
+
>
|
|
443
|
+
⚠
|
|
444
|
+
</span>
|
|
445
|
+
)}
|
|
446
|
+
</span>
|
|
447
|
+
</Sidebar.Item>
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
450
|
+
</Sidebar.Section>
|
|
451
|
+
)}
|
|
374
452
|
|
|
375
453
|
{flatItems.length === 0 && (
|
|
376
|
-
<
|
|
454
|
+
<Box paddingX="lg" paddingY="sm" style={{ textAlign: 'center' }}>
|
|
377
455
|
<Text size="sm" color="tertiary">No results</Text>
|
|
378
|
-
</
|
|
456
|
+
</Box>
|
|
379
457
|
)}
|
|
380
458
|
</div>
|
|
381
459
|
</Sidebar.Nav>
|
|
382
460
|
|
|
383
461
|
{/* Footer */}
|
|
384
462
|
<Sidebar.Footer>
|
|
385
|
-
<
|
|
463
|
+
<Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
|
|
386
464
|
<Text size="xs" color="tertiary">
|
|
387
465
|
{isFilterActive || searchResults ? `${flatItems.length} of ${fragments.length}` : fragments.length} components
|
|
388
466
|
</Text>
|
|
389
467
|
<Sidebar.CollapseToggle />
|
|
390
|
-
</
|
|
468
|
+
</Stack>
|
|
391
469
|
</Sidebar.Footer>
|
|
392
470
|
</>
|
|
393
471
|
);
|