@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton Loading Components
|
|
3
|
+
*
|
|
4
|
+
* Shows animated placeholders while the app is loading.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import clsx from "clsx";
|
|
8
|
+
|
|
9
|
+
interface SkeletonProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function Skeleton({ className }: SkeletonProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={clsx(
|
|
17
|
+
"animate-pulse bg-[--bg-hover] rounded",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Full app skeleton shown during initial load
|
|
26
|
+
*/
|
|
27
|
+
export function AppSkeleton() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex h-screen bg-[--bg-primary]">
|
|
30
|
+
{/* Sidebar skeleton */}
|
|
31
|
+
<div className="w-60 border-r border-[--border] bg-[--bg-secondary] flex flex-col">
|
|
32
|
+
{/* Header */}
|
|
33
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-[--border]">
|
|
34
|
+
<Skeleton className="w-20 h-5" />
|
|
35
|
+
<Skeleton className="w-6 h-6 rounded-md" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Search */}
|
|
39
|
+
<div className="px-3 py-3">
|
|
40
|
+
<Skeleton className="w-full h-8 rounded-md" />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Component list */}
|
|
44
|
+
<div className="flex-1 px-2 space-y-4 overflow-hidden">
|
|
45
|
+
{/* Category 1 */}
|
|
46
|
+
<div>
|
|
47
|
+
<Skeleton className="w-16 h-3 mx-2 mb-2" />
|
|
48
|
+
<div className="space-y-1">
|
|
49
|
+
<Skeleton className="w-full h-7 rounded-md" />
|
|
50
|
+
<Skeleton className="w-full h-7 rounded-md" />
|
|
51
|
+
<Skeleton className="w-3/4 h-7 rounded-md" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Category 2 */}
|
|
56
|
+
<div>
|
|
57
|
+
<Skeleton className="w-20 h-3 mx-2 mb-2" />
|
|
58
|
+
<div className="space-y-1">
|
|
59
|
+
<Skeleton className="w-full h-7 rounded-md" />
|
|
60
|
+
<Skeleton className="w-5/6 h-7 rounded-md" />
|
|
61
|
+
<Skeleton className="w-full h-7 rounded-md" />
|
|
62
|
+
<Skeleton className="w-2/3 h-7 rounded-md" />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Category 3 */}
|
|
67
|
+
<div>
|
|
68
|
+
<Skeleton className="w-24 h-3 mx-2 mb-2" />
|
|
69
|
+
<div className="space-y-1">
|
|
70
|
+
<Skeleton className="w-4/5 h-7 rounded-md" />
|
|
71
|
+
<Skeleton className="w-full h-7 rounded-md" />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Footer */}
|
|
77
|
+
<div className="px-4 py-3 border-t border-[--border-subtle]">
|
|
78
|
+
<Skeleton className="w-24 h-3" />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Main content skeleton */}
|
|
83
|
+
<div className="flex-1 flex flex-col">
|
|
84
|
+
{/* Toolbar */}
|
|
85
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-[--border] bg-[--bg-secondary]">
|
|
86
|
+
<div className="flex items-center gap-3">
|
|
87
|
+
<Skeleton className="w-24 h-5" />
|
|
88
|
+
<Skeleton className="w-16 h-4" />
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
<Skeleton className="w-16 h-7 rounded" />
|
|
92
|
+
<Skeleton className="w-24 h-7 rounded" />
|
|
93
|
+
<Skeleton className="w-20 h-7 rounded" />
|
|
94
|
+
<Skeleton className="w-6 h-6 rounded" />
|
|
95
|
+
<Skeleton className="w-6 h-6 rounded" />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Variant tabs */}
|
|
100
|
+
<div className="px-4 py-2 border-b border-[--border] bg-[--bg-primary]">
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<Skeleton className="w-16 h-7 rounded-md" />
|
|
103
|
+
<Skeleton className="w-20 h-7 rounded-md" />
|
|
104
|
+
<Skeleton className="w-14 h-7 rounded-md" />
|
|
105
|
+
<Skeleton className="w-18 h-7 rounded-md" />
|
|
106
|
+
<Skeleton className="w-12 h-7 rounded-md" />
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Preview area */}
|
|
111
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
112
|
+
<Skeleton className="w-64 h-32 rounded-lg" />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Bottom panel */}
|
|
116
|
+
<div className="h-64 border-t border-[--border] bg-[--bg-secondary]">
|
|
117
|
+
<div className="flex items-center gap-1 px-4 h-10 border-b border-[--border-subtle]">
|
|
118
|
+
<Skeleton className="w-12 h-6 rounded" />
|
|
119
|
+
<Skeleton className="w-10 h-6 rounded" />
|
|
120
|
+
<Skeleton className="w-12 h-6 rounded" />
|
|
121
|
+
</div>
|
|
122
|
+
<div className="p-4 space-y-3">
|
|
123
|
+
<div className="flex items-center gap-4">
|
|
124
|
+
<Skeleton className="w-20 h-4" />
|
|
125
|
+
<Skeleton className="w-32 h-8 rounded" />
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-4">
|
|
128
|
+
<Skeleton className="w-16 h-4" />
|
|
129
|
+
<Skeleton className="w-24 h-8 rounded" />
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex items-center gap-4">
|
|
132
|
+
<Skeleton className="w-24 h-4" />
|
|
133
|
+
<Skeleton className="w-20 h-8 rounded" />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Component preview skeleton
|
|
144
|
+
*/
|
|
145
|
+
export function PreviewSkeleton() {
|
|
146
|
+
return (
|
|
147
|
+
<div className="flex items-center justify-center p-8">
|
|
148
|
+
<div className="text-center space-y-3">
|
|
149
|
+
<div className="animate-spin w-8 h-8 border-2 border-[--border] border-t-[--color-accent] rounded-full mx-auto" />
|
|
150
|
+
<p className="text-sm text-tertiary">Loading component...</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default AppSkeleton;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, type ReactNode } from "react";
|
|
2
|
+
import type { SegmentVariant } from "../../core/index.js";
|
|
3
|
+
|
|
4
|
+
interface StoryRendererProps {
|
|
5
|
+
/** The variant to render */
|
|
6
|
+
variant: SegmentVariant;
|
|
7
|
+
/** Children render function - receives rendered content */
|
|
8
|
+
children: (content: ReactNode | null, isLoading: boolean, error: Error | null) => ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Component that handles async loader execution before rendering a story variant.
|
|
13
|
+
*
|
|
14
|
+
* If the variant has loaders:
|
|
15
|
+
* 1. Shows loading state while loaders execute
|
|
16
|
+
* 2. Merges all loaded data
|
|
17
|
+
* 3. Passes loaded data to render function
|
|
18
|
+
*
|
|
19
|
+
* If no loaders, renders immediately.
|
|
20
|
+
*/
|
|
21
|
+
export function StoryRenderer({ variant, children }: StoryRendererProps) {
|
|
22
|
+
const [loadedData, setLoadedData] = useState<Record<string, unknown> | null>(null);
|
|
23
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
24
|
+
const [error, setError] = useState<Error | null>(null);
|
|
25
|
+
|
|
26
|
+
const hasLoaders = variant.loaders && variant.loaders.length > 0;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Reset state when variant changes
|
|
30
|
+
setLoadedData(null);
|
|
31
|
+
setError(null);
|
|
32
|
+
|
|
33
|
+
if (!hasLoaders) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
|
|
40
|
+
async function executeLoaders() {
|
|
41
|
+
try {
|
|
42
|
+
// Execute all loaders in parallel
|
|
43
|
+
const results = await Promise.all(
|
|
44
|
+
variant.loaders!.map((loader) => loader())
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (cancelled) return;
|
|
48
|
+
|
|
49
|
+
// Merge all loaded data (later loaders override earlier ones)
|
|
50
|
+
const merged = results.reduce(
|
|
51
|
+
(acc, result) => ({ ...acc, ...result }),
|
|
52
|
+
{}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
setLoadedData(merged);
|
|
56
|
+
setError(null);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (cancelled) return;
|
|
59
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
60
|
+
} finally {
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
executeLoaders();
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
cancelled = true;
|
|
71
|
+
};
|
|
72
|
+
}, [variant, hasLoaders]);
|
|
73
|
+
|
|
74
|
+
// If loading, pass null content with loading flag
|
|
75
|
+
if (hasLoaders && isLoading) {
|
|
76
|
+
return <>{children(null, true, null)}</>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If error during loading, pass error
|
|
80
|
+
if (error) {
|
|
81
|
+
return <>{children(null, false, error)}</>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build render options with loaded data only
|
|
85
|
+
const renderOptions = useMemo(() => ({
|
|
86
|
+
loadedData: hasLoaders ? loadedData ?? undefined : undefined,
|
|
87
|
+
}), [hasLoaders, loadedData]);
|
|
88
|
+
|
|
89
|
+
// Render the variant with options
|
|
90
|
+
try {
|
|
91
|
+
const content = variant.render(renderOptions);
|
|
92
|
+
return <>{children(content, false, null)}</>;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const renderError = err instanceof Error ? err : new Error(String(err));
|
|
95
|
+
return <>{children(null, false, renderError)}</>;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Loading indicator for stories with loaders
|
|
101
|
+
*/
|
|
102
|
+
export function LoaderIndicator() {
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex items-center gap-2 text-secondary text-sm">
|
|
105
|
+
<svg
|
|
106
|
+
className="animate-spin h-4 w-4"
|
|
107
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
108
|
+
fill="none"
|
|
109
|
+
viewBox="0 0 24 24"
|
|
110
|
+
>
|
|
111
|
+
<circle
|
|
112
|
+
className="opacity-25"
|
|
113
|
+
cx="12"
|
|
114
|
+
cy="12"
|
|
115
|
+
r="10"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
strokeWidth="4"
|
|
118
|
+
/>
|
|
119
|
+
<path
|
|
120
|
+
className="opacity-75"
|
|
121
|
+
fill="currentColor"
|
|
122
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
<span>Running loaders...</span>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
|
2
|
+
import { BRAND } from '../../core/index.js';
|
|
3
|
+
|
|
4
|
+
type Theme = 'light' | 'dark' | 'system';
|
|
5
|
+
|
|
6
|
+
interface ThemeContextValue {
|
|
7
|
+
theme: Theme;
|
|
8
|
+
resolvedTheme: 'light' | 'dark';
|
|
9
|
+
setTheme: (theme: Theme) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
const STORAGE_KEY = `${BRAND.storagePrefix}theme`;
|
|
15
|
+
|
|
16
|
+
function getSystemTheme(): 'light' | 'dark' {
|
|
17
|
+
if (typeof window === 'undefined') return 'light';
|
|
18
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStoredTheme(): Theme {
|
|
22
|
+
if (typeof window === 'undefined') return 'system';
|
|
23
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
24
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
25
|
+
return stored;
|
|
26
|
+
}
|
|
27
|
+
return 'system';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
31
|
+
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
|
|
32
|
+
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
|
|
33
|
+
const stored = getStoredTheme();
|
|
34
|
+
return stored === 'system' ? getSystemTheme() : stored;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const setTheme = useCallback((newTheme: Theme) => {
|
|
38
|
+
setThemeState(newTheme);
|
|
39
|
+
localStorage.setItem(STORAGE_KEY, newTheme);
|
|
40
|
+
|
|
41
|
+
// Immediately update resolved theme to avoid timing issues
|
|
42
|
+
const resolved = newTheme === 'system' ? getSystemTheme() : newTheme;
|
|
43
|
+
setResolvedTheme(resolved);
|
|
44
|
+
|
|
45
|
+
// Immediately apply to document
|
|
46
|
+
if (resolved === 'dark') {
|
|
47
|
+
document.documentElement.classList.add('dark');
|
|
48
|
+
} else {
|
|
49
|
+
document.documentElement.classList.remove('dark');
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Update resolved theme and apply to document
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const resolved = theme === 'system' ? getSystemTheme() : theme;
|
|
56
|
+
setResolvedTheme(resolved);
|
|
57
|
+
|
|
58
|
+
if (resolved === 'dark') {
|
|
59
|
+
document.documentElement.classList.add('dark');
|
|
60
|
+
} else {
|
|
61
|
+
document.documentElement.classList.remove('dark');
|
|
62
|
+
}
|
|
63
|
+
}, [theme]);
|
|
64
|
+
|
|
65
|
+
// Listen for system theme changes
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (theme !== 'system') return;
|
|
68
|
+
|
|
69
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
70
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
71
|
+
setResolvedTheme(e.matches ? 'dark' : 'light');
|
|
72
|
+
if (e.matches) {
|
|
73
|
+
document.documentElement.classList.add('dark');
|
|
74
|
+
} else {
|
|
75
|
+
document.documentElement.classList.remove('dark');
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
mediaQuery.addEventListener('change', handler);
|
|
80
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
81
|
+
}, [theme]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
|
|
85
|
+
{children}
|
|
86
|
+
</ThemeContext.Provider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useTheme() {
|
|
91
|
+
const context = useContext(ThemeContext);
|
|
92
|
+
if (!context) {
|
|
93
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
94
|
+
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { CheckIcon, WarningIcon, RefreshIcon } from './Icons.js';
|
|
3
|
+
|
|
4
|
+
export interface ToastMessage {
|
|
5
|
+
id: string;
|
|
6
|
+
type: 'success' | 'error' | 'info';
|
|
7
|
+
message: string;
|
|
8
|
+
duration?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ToastProps {
|
|
12
|
+
messages: ToastMessage[];
|
|
13
|
+
onDismiss: (id: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Toast({ messages, onDismiss }: ToastProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
|
19
|
+
{messages.map((toast) => (
|
|
20
|
+
<ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ToastItem({ toast, onDismiss }: { toast: ToastMessage; onDismiss: (id: string) => void }) {
|
|
27
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Animate in
|
|
31
|
+
requestAnimationFrame(() => setIsVisible(true));
|
|
32
|
+
|
|
33
|
+
// Auto-dismiss
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
setIsVisible(false);
|
|
36
|
+
setTimeout(() => onDismiss(toast.id), 200);
|
|
37
|
+
}, toast.duration || 3000);
|
|
38
|
+
|
|
39
|
+
return () => clearTimeout(timer);
|
|
40
|
+
}, [toast.id, toast.duration, onDismiss]);
|
|
41
|
+
|
|
42
|
+
const iconMap = {
|
|
43
|
+
success: <CheckIcon className="w-4 h-4 text-emerald-500" />,
|
|
44
|
+
error: <WarningIcon className="w-4 h-4 text-red-500" />,
|
|
45
|
+
info: <RefreshIcon className="w-4 h-4 text-blue-500" />,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const bgMap = {
|
|
49
|
+
success: 'bg-emerald-500/10 border-emerald-500/20',
|
|
50
|
+
error: 'bg-red-500/10 border-red-500/20',
|
|
51
|
+
info: 'bg-blue-500/10 border-blue-500/20',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={`
|
|
57
|
+
flex items-center gap-2 px-3 py-2 rounded-lg border backdrop-blur-sm
|
|
58
|
+
shadow-lg transition-all duration-200 ease-out
|
|
59
|
+
${bgMap[toast.type]}
|
|
60
|
+
${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'}
|
|
61
|
+
`}
|
|
62
|
+
>
|
|
63
|
+
{iconMap[toast.type]}
|
|
64
|
+
<span className="text-sm text-primary font-medium">{toast.message}</span>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|