@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,761 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime adapter for converting Storybook CSF modules to Segment definitions.
|
|
3
|
+
*
|
|
4
|
+
* This operates on IMPORTED modules at runtime, not source code parsing.
|
|
5
|
+
* By leveraging Vite's module system, we get 100% accurate render functions
|
|
6
|
+
* without any regex or AST parsing complexity.
|
|
7
|
+
*
|
|
8
|
+
* Supports Storybook 8.x with both CSF2 (Template.bind) and CSF3 (object stories).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createElement, type ComponentType, type ReactNode } from "react";
|
|
12
|
+
import { toId, storyNameFromExport, isExportStory } from "@storybook/csf";
|
|
13
|
+
import type {
|
|
14
|
+
SegmentDefinition,
|
|
15
|
+
SegmentMeta,
|
|
16
|
+
SegmentUsage,
|
|
17
|
+
PropDefinition,
|
|
18
|
+
SegmentVariant,
|
|
19
|
+
ControlType,
|
|
20
|
+
VariantLoader,
|
|
21
|
+
PlayFunction,
|
|
22
|
+
PlayFunctionContext,
|
|
23
|
+
VariantRenderOptions,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
// Re-export @storybook/csf utilities for use in other modules
|
|
27
|
+
export { toId, storyNameFromExport, isExportStory };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Storybook decorator function signature
|
|
31
|
+
*/
|
|
32
|
+
export type Decorator = (
|
|
33
|
+
Story: () => ReactNode,
|
|
34
|
+
context: StoryContext
|
|
35
|
+
) => ReactNode;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Storybook loader function signature
|
|
39
|
+
*/
|
|
40
|
+
export type Loader = (context: StoryContext) => Promise<Record<string, unknown>>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Storybook play function signature (internal, extends StoryContext)
|
|
44
|
+
*/
|
|
45
|
+
type StorybookPlayFunction = (context: StorybookPlayFunctionContext) => Promise<void>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Context passed to Storybook play functions (extends StoryContext for compatibility)
|
|
49
|
+
*/
|
|
50
|
+
interface StorybookPlayFunctionContext extends StoryContext {
|
|
51
|
+
canvasElement: HTMLElement;
|
|
52
|
+
step: (name: string, fn: () => Promise<void>) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Context passed to decorators and render functions
|
|
57
|
+
*/
|
|
58
|
+
export interface StoryContext {
|
|
59
|
+
args: Record<string, unknown>;
|
|
60
|
+
argTypes: Record<string, StoryArgType>;
|
|
61
|
+
globals: Record<string, unknown>;
|
|
62
|
+
parameters: Record<string, unknown>;
|
|
63
|
+
id: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
name: string;
|
|
66
|
+
story: string;
|
|
67
|
+
viewMode: "story" | "docs";
|
|
68
|
+
loaded: Record<string, unknown>;
|
|
69
|
+
abortSignal: AbortSignal;
|
|
70
|
+
componentId: string;
|
|
71
|
+
title: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Storybook Meta (default export)
|
|
76
|
+
*/
|
|
77
|
+
export interface StoryMeta {
|
|
78
|
+
title?: string;
|
|
79
|
+
component?: ComponentType<unknown>;
|
|
80
|
+
subcomponents?: Record<string, ComponentType<unknown>>;
|
|
81
|
+
tags?: string[];
|
|
82
|
+
parameters?: Record<string, unknown> & {
|
|
83
|
+
docs?: {
|
|
84
|
+
description?: {
|
|
85
|
+
component?: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
argTypes?: Record<string, StoryArgType>;
|
|
90
|
+
args?: Record<string, unknown>;
|
|
91
|
+
decorators?: Decorator[];
|
|
92
|
+
loaders?: Loader[];
|
|
93
|
+
render?: (args: Record<string, unknown>, context?: StoryContext) => ReactNode;
|
|
94
|
+
// Story filtering
|
|
95
|
+
includeStories?: string[] | RegExp;
|
|
96
|
+
excludeStories?: string[] | RegExp;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Storybook argType definition
|
|
101
|
+
*/
|
|
102
|
+
export interface StoryArgType {
|
|
103
|
+
control?:
|
|
104
|
+
| string
|
|
105
|
+
| false
|
|
106
|
+
| { type: string; min?: number; max?: number; step?: number; presetColors?: string[] };
|
|
107
|
+
options?: string[];
|
|
108
|
+
description?: string;
|
|
109
|
+
table?: {
|
|
110
|
+
defaultValue?: { summary: string };
|
|
111
|
+
type?: { summary: string };
|
|
112
|
+
category?: string;
|
|
113
|
+
subcategory?: string;
|
|
114
|
+
disable?: boolean;
|
|
115
|
+
};
|
|
116
|
+
type?: { name: string; required?: boolean };
|
|
117
|
+
name?: string;
|
|
118
|
+
defaultValue?: unknown;
|
|
119
|
+
if?: { arg?: string; exists?: boolean };
|
|
120
|
+
mapping?: Record<string, unknown>;
|
|
121
|
+
action?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Storybook Story export (CSF3)
|
|
126
|
+
*/
|
|
127
|
+
export interface Story {
|
|
128
|
+
args?: Record<string, unknown>;
|
|
129
|
+
argTypes?: Record<string, StoryArgType>;
|
|
130
|
+
render?: (args: Record<string, unknown>, context?: StoryContext) => ReactNode;
|
|
131
|
+
decorators?: Decorator[];
|
|
132
|
+
loaders?: Loader[];
|
|
133
|
+
play?: StorybookPlayFunction;
|
|
134
|
+
parameters?: Record<string, unknown> & {
|
|
135
|
+
docs?: {
|
|
136
|
+
description?: {
|
|
137
|
+
story?: string;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
name?: string;
|
|
142
|
+
storyName?: string; // Legacy CSF2
|
|
143
|
+
tags?: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* CSF2 story function (from Template.bind({})) with args attached
|
|
148
|
+
*/
|
|
149
|
+
export type CSF2Story = ((args: Record<string, unknown>) => ReactNode) & {
|
|
150
|
+
args?: Record<string, unknown>;
|
|
151
|
+
argTypes?: Record<string, StoryArgType>;
|
|
152
|
+
decorators?: Decorator[];
|
|
153
|
+
loaders?: Loader[];
|
|
154
|
+
play?: StorybookPlayFunction;
|
|
155
|
+
parameters?: Record<string, unknown>;
|
|
156
|
+
storyName?: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A complete Storybook module with default meta and named story exports
|
|
161
|
+
*/
|
|
162
|
+
export interface StoryModule {
|
|
163
|
+
default: StoryMeta;
|
|
164
|
+
[exportName: string]: Story | CSF2Story | StoryMeta | unknown;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Global configuration from preview.tsx
|
|
169
|
+
*/
|
|
170
|
+
export interface PreviewConfig {
|
|
171
|
+
decorators?: Decorator[];
|
|
172
|
+
parameters?: Record<string, unknown>;
|
|
173
|
+
globalTypes?: Record<string, unknown>;
|
|
174
|
+
args?: Record<string, unknown>;
|
|
175
|
+
argTypes?: Record<string, StoryArgType>;
|
|
176
|
+
loaders?: Loader[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Store for global preview config (set by previewLoader)
|
|
180
|
+
let globalPreviewConfig: PreviewConfig = {};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set the global preview configuration loaded from .storybook/preview.tsx
|
|
184
|
+
*/
|
|
185
|
+
export function setPreviewConfig(config: PreviewConfig): void {
|
|
186
|
+
globalPreviewConfig = config;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the current global preview configuration
|
|
191
|
+
*/
|
|
192
|
+
export function getPreviewConfig(): PreviewConfig {
|
|
193
|
+
return globalPreviewConfig;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Convert a Storybook module to a Segment definition at runtime.
|
|
198
|
+
*
|
|
199
|
+
* @param storyModule - The imported Storybook module
|
|
200
|
+
* @param filePath - File path for metadata extraction
|
|
201
|
+
* @returns A complete SegmentDefinition ready for the viewer
|
|
202
|
+
*/
|
|
203
|
+
export function storyModuleToSegment(
|
|
204
|
+
storyModule: StoryModule,
|
|
205
|
+
filePath: string
|
|
206
|
+
): SegmentDefinition | null {
|
|
207
|
+
const meta = storyModule.default;
|
|
208
|
+
const component = meta.component;
|
|
209
|
+
|
|
210
|
+
// Stories without a component (e.g., documentation pages, icon galleries) are skipped
|
|
211
|
+
if (!component) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const componentName = extractComponentName(meta, filePath);
|
|
216
|
+
const category = extractCategory(meta.title);
|
|
217
|
+
const props = convertArgTypes(meta.argTypes ?? {}, globalPreviewConfig.argTypes);
|
|
218
|
+
const variants = extractVariants(storyModule, component, meta);
|
|
219
|
+
|
|
220
|
+
// Extract Figma URL from parameters.design.url (storybook-addon-designs) or parameters.figma
|
|
221
|
+
const figmaUrl = extractFigmaUrl(meta.parameters);
|
|
222
|
+
|
|
223
|
+
const segmentMeta: SegmentMeta = {
|
|
224
|
+
name: componentName,
|
|
225
|
+
description:
|
|
226
|
+
meta.parameters?.docs?.description?.component ??
|
|
227
|
+
`${componentName} component`,
|
|
228
|
+
category,
|
|
229
|
+
tags: meta.tags?.filter((t) => t !== "autodocs"),
|
|
230
|
+
status: "stable",
|
|
231
|
+
figma: figmaUrl,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const usage: SegmentUsage = {
|
|
235
|
+
when: [`Use ${componentName} for its intended purpose`],
|
|
236
|
+
whenNot: ["When a more specific component is available"],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
component,
|
|
241
|
+
meta: segmentMeta,
|
|
242
|
+
usage,
|
|
243
|
+
props,
|
|
244
|
+
variants,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract component name from meta or file path
|
|
250
|
+
*/
|
|
251
|
+
function extractComponentName(meta: StoryMeta, filePath: string): string {
|
|
252
|
+
// Try title (last segment of path like "Components/Forms/Button" -> "Button")
|
|
253
|
+
if (meta.title) {
|
|
254
|
+
const parts = meta.title.split("/");
|
|
255
|
+
return parts[parts.length - 1];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try component displayName
|
|
259
|
+
if (meta.component?.displayName) {
|
|
260
|
+
return meta.component.displayName;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try component name
|
|
264
|
+
if (meta.component?.name && meta.component.name !== "Component") {
|
|
265
|
+
return meta.component.name;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback: extract from file path
|
|
269
|
+
const match = filePath.match(/([^/\\]+)\.stories\.(tsx?|jsx?)$/);
|
|
270
|
+
return match?.[1] ?? "Unknown";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract category from Storybook title path
|
|
275
|
+
*/
|
|
276
|
+
function extractCategory(title?: string): string {
|
|
277
|
+
if (!title) return "general";
|
|
278
|
+
|
|
279
|
+
const parts = title.split("/");
|
|
280
|
+
// "Components/Forms/Button" -> "forms" (need at least 3 parts for a subcategory)
|
|
281
|
+
if (parts.length >= 3) {
|
|
282
|
+
return parts[parts.length - 2].toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
// "Components/Button" -> "general" (no subcategory specified)
|
|
285
|
+
return "general";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Extract Figma URL from Storybook parameters
|
|
290
|
+
* Supports storybook-addon-designs format and custom figma parameter
|
|
291
|
+
*/
|
|
292
|
+
function extractFigmaUrl(parameters?: Record<string, unknown>): string | undefined {
|
|
293
|
+
if (!parameters) return undefined;
|
|
294
|
+
|
|
295
|
+
// Try storybook-addon-designs format: parameters.design.url
|
|
296
|
+
const design = parameters.design as { url?: string; type?: string } | undefined;
|
|
297
|
+
if (design?.url && typeof design.url === "string") {
|
|
298
|
+
return design.url;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Try custom figma parameter: parameters.figma
|
|
302
|
+
if (typeof parameters.figma === "string") {
|
|
303
|
+
return parameters.figma;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Convert Storybook argTypes to Segment props
|
|
311
|
+
* Merges global argTypes from preview config with meta argTypes
|
|
312
|
+
*/
|
|
313
|
+
function convertArgTypes(
|
|
314
|
+
argTypes: Record<string, StoryArgType>,
|
|
315
|
+
globalArgTypes?: Record<string, StoryArgType>
|
|
316
|
+
): Record<string, PropDefinition> {
|
|
317
|
+
const props: Record<string, PropDefinition> = {};
|
|
318
|
+
|
|
319
|
+
// Merge global and meta argTypes (meta takes precedence)
|
|
320
|
+
const mergedArgTypes = { ...globalArgTypes, ...argTypes };
|
|
321
|
+
|
|
322
|
+
for (const [name, argType] of Object.entries(mergedArgTypes)) {
|
|
323
|
+
// Skip disabled argTypes
|
|
324
|
+
if (argType.table?.disable) continue;
|
|
325
|
+
// Skip action-only argTypes (no control)
|
|
326
|
+
if (argType.control === false && argType.action) continue;
|
|
327
|
+
|
|
328
|
+
// Extract control type and options
|
|
329
|
+
const { controlType, controlOptions } = extractControlInfo(argType);
|
|
330
|
+
|
|
331
|
+
props[name] = {
|
|
332
|
+
type: inferPropType(argType),
|
|
333
|
+
description: argType.description ?? `${name} prop`,
|
|
334
|
+
...(argType.options && { values: argType.options }),
|
|
335
|
+
...(argType.table?.defaultValue && {
|
|
336
|
+
default: argType.table.defaultValue.summary,
|
|
337
|
+
}),
|
|
338
|
+
...(argType.defaultValue !== undefined && {
|
|
339
|
+
default: argType.defaultValue,
|
|
340
|
+
}),
|
|
341
|
+
...(argType.type?.required && { required: true }),
|
|
342
|
+
...(controlType && { controlType }),
|
|
343
|
+
...(controlOptions && Object.keys(controlOptions).length > 0 && { controlOptions }),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return props;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Extract control type and options from a Storybook argType
|
|
352
|
+
*/
|
|
353
|
+
function extractControlInfo(argType: StoryArgType): {
|
|
354
|
+
controlType?: ControlType;
|
|
355
|
+
controlOptions?: PropDefinition["controlOptions"];
|
|
356
|
+
} {
|
|
357
|
+
// Handle no control or explicitly disabled control
|
|
358
|
+
if (argType.control === undefined || argType.control === false) {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const control = typeof argType.control === "string"
|
|
363
|
+
? { type: argType.control }
|
|
364
|
+
: argType.control;
|
|
365
|
+
|
|
366
|
+
// Map control type string to ControlType
|
|
367
|
+
const validControlTypes: ControlType[] = [
|
|
368
|
+
"text", "number", "range", "boolean", "select", "multi-select",
|
|
369
|
+
"radio", "inline-radio", "check", "inline-check", "object", "file", "color", "date"
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const controlType = validControlTypes.includes(control.type as ControlType)
|
|
373
|
+
? (control.type as ControlType)
|
|
374
|
+
: undefined;
|
|
375
|
+
|
|
376
|
+
// Extract control options for controls that need them
|
|
377
|
+
const controlOptions: PropDefinition["controlOptions"] = {};
|
|
378
|
+
|
|
379
|
+
if (control.min !== undefined) controlOptions.min = control.min;
|
|
380
|
+
if (control.max !== undefined) controlOptions.max = control.max;
|
|
381
|
+
if (control.step !== undefined) controlOptions.step = control.step;
|
|
382
|
+
if (control.presetColors) controlOptions.presetColors = control.presetColors;
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
controlType,
|
|
386
|
+
controlOptions: Object.keys(controlOptions).length > 0 ? controlOptions : undefined,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Infer prop type from Storybook control/type
|
|
392
|
+
* Handles all Storybook 8.x control types
|
|
393
|
+
*/
|
|
394
|
+
function inferPropType(argType: StoryArgType): PropDefinition["type"] {
|
|
395
|
+
// Action argType → function
|
|
396
|
+
if (argType.action) return "function";
|
|
397
|
+
|
|
398
|
+
// If has options, it's an enum
|
|
399
|
+
if (argType.options?.length) return "enum";
|
|
400
|
+
|
|
401
|
+
// Check explicit type
|
|
402
|
+
if (argType.type?.name) {
|
|
403
|
+
const typeMap: Record<string, PropDefinition["type"]> = {
|
|
404
|
+
string: "string",
|
|
405
|
+
number: "number",
|
|
406
|
+
boolean: "boolean",
|
|
407
|
+
object: "object",
|
|
408
|
+
array: "array",
|
|
409
|
+
function: "function",
|
|
410
|
+
};
|
|
411
|
+
const mapped = typeMap[argType.type.name];
|
|
412
|
+
if (mapped) return mapped;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check control type
|
|
416
|
+
const control =
|
|
417
|
+
typeof argType.control === "string"
|
|
418
|
+
? argType.control
|
|
419
|
+
: argType.control
|
|
420
|
+
? argType.control.type
|
|
421
|
+
: undefined;
|
|
422
|
+
|
|
423
|
+
if (control) {
|
|
424
|
+
const controlMap: Record<string, PropDefinition["type"]> = {
|
|
425
|
+
// Text controls
|
|
426
|
+
text: "string",
|
|
427
|
+
|
|
428
|
+
// Number controls
|
|
429
|
+
number: "number",
|
|
430
|
+
range: "number",
|
|
431
|
+
|
|
432
|
+
// Boolean controls
|
|
433
|
+
boolean: "boolean",
|
|
434
|
+
check: "boolean",
|
|
435
|
+
"inline-check": "boolean",
|
|
436
|
+
|
|
437
|
+
// Enum/selection controls
|
|
438
|
+
select: "enum",
|
|
439
|
+
"multi-select": "enum",
|
|
440
|
+
radio: "enum",
|
|
441
|
+
"inline-radio": "enum",
|
|
442
|
+
|
|
443
|
+
// Object controls
|
|
444
|
+
object: "object",
|
|
445
|
+
file: "object",
|
|
446
|
+
|
|
447
|
+
// Special string controls
|
|
448
|
+
color: "string",
|
|
449
|
+
date: "string",
|
|
450
|
+
};
|
|
451
|
+
const mapped = controlMap[control];
|
|
452
|
+
if (mapped) return mapped;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return "string";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Check if a value looks like a Storybook story
|
|
460
|
+
* Handles both CSF 3 (objects) and CSF 2 (functions from Template.bind({}))
|
|
461
|
+
*/
|
|
462
|
+
function isStory(value: unknown): value is Story | CSF2Story {
|
|
463
|
+
// CSF 3: Story is an object with args/render/play
|
|
464
|
+
if (typeof value === "object" && value !== null) {
|
|
465
|
+
const obj = value as Record<string, unknown>;
|
|
466
|
+
if ("args" in obj || "render" in obj || "play" in obj) return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// CSF 2: Story is a function (from Template.bind({})) with args attached
|
|
470
|
+
if (typeof value === "function") {
|
|
471
|
+
const fn = value as ((...args: unknown[]) => unknown) & { args?: unknown };
|
|
472
|
+
if ("args" in fn) return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Extract variants from story exports using @storybook/csf utilities
|
|
480
|
+
*/
|
|
481
|
+
function extractVariants(
|
|
482
|
+
storyModule: StoryModule,
|
|
483
|
+
component: ComponentType<unknown>,
|
|
484
|
+
meta: StoryMeta
|
|
485
|
+
): SegmentVariant[] {
|
|
486
|
+
const variants: SegmentVariant[] = [];
|
|
487
|
+
|
|
488
|
+
for (const [exportName, exportValue] of Object.entries(storyModule)) {
|
|
489
|
+
// Skip default export
|
|
490
|
+
if (exportName === "default") continue;
|
|
491
|
+
|
|
492
|
+
// Use isExportStory to filter based on includeStories/excludeStories
|
|
493
|
+
if (!isExportStory(exportName, meta)) continue;
|
|
494
|
+
|
|
495
|
+
// Check if it's a story
|
|
496
|
+
if (!isStory(exportValue)) continue;
|
|
497
|
+
|
|
498
|
+
const story = exportValue as Story | CSF2Story;
|
|
499
|
+
|
|
500
|
+
// Get story name using storyNameFromExport
|
|
501
|
+
const storyName =
|
|
502
|
+
(typeof story === "object" && story.name) ||
|
|
503
|
+
(typeof story === "object" && story.storyName) ||
|
|
504
|
+
(typeof story === "function" && story.storyName) ||
|
|
505
|
+
storyNameFromExport(exportName);
|
|
506
|
+
|
|
507
|
+
// Generate story ID matching Storybook format
|
|
508
|
+
const storyId = toId(meta.title || "Unknown", exportName);
|
|
509
|
+
|
|
510
|
+
// Extract description based on story format
|
|
511
|
+
let description = `${storyName} variant`;
|
|
512
|
+
if (typeof story === "object" && story.parameters?.docs?.description?.story) {
|
|
513
|
+
description = story.parameters.docs.description.story;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check for play function and capture it
|
|
517
|
+
const storyPlayFn = typeof story === "object" ? story.play : story.play;
|
|
518
|
+
const hasPlayFunction = !!storyPlayFn;
|
|
519
|
+
|
|
520
|
+
// Create wrapped play function that adapts Storybook context to our PlayFunctionContext
|
|
521
|
+
const wrappedPlay: PlayFunction | undefined = storyPlayFn
|
|
522
|
+
? async (context: PlayFunctionContext): Promise<void> => {
|
|
523
|
+
// Build full Storybook context for compatibility
|
|
524
|
+
const args = {
|
|
525
|
+
...globalPreviewConfig.args,
|
|
526
|
+
...meta.args,
|
|
527
|
+
...(typeof story === "function" ? story.args : story.args),
|
|
528
|
+
};
|
|
529
|
+
const fullContext = buildStoryContext(meta, story, args, storyId, storyName);
|
|
530
|
+
|
|
531
|
+
// Merge our context with Storybook context
|
|
532
|
+
const playContext = {
|
|
533
|
+
...fullContext,
|
|
534
|
+
canvasElement: context.canvasElement,
|
|
535
|
+
args: context.args,
|
|
536
|
+
step: context.step,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
await storyPlayFn(playContext as unknown as StorybookPlayFunctionContext);
|
|
540
|
+
}
|
|
541
|
+
: undefined;
|
|
542
|
+
|
|
543
|
+
// Get story tags
|
|
544
|
+
const storyTags = typeof story === "object" ? story.tags : undefined;
|
|
545
|
+
|
|
546
|
+
// Collect loaders from global, meta, and story (in order)
|
|
547
|
+
const loaders = collectLoaders(meta, story);
|
|
548
|
+
|
|
549
|
+
// Compute the merged args for this variant (for code generation)
|
|
550
|
+
const variantArgs = {
|
|
551
|
+
...globalPreviewConfig.args,
|
|
552
|
+
...meta.args,
|
|
553
|
+
...(typeof story === "function" ? story.args : story.args),
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// Only include args if there are any defined
|
|
557
|
+
const hasArgs = Object.keys(variantArgs).length > 0;
|
|
558
|
+
|
|
559
|
+
variants.push({
|
|
560
|
+
name: storyName,
|
|
561
|
+
description,
|
|
562
|
+
render: createRenderFunction(story, component, meta, storyId, storyName),
|
|
563
|
+
// Store Storybook-specific metadata
|
|
564
|
+
...(hasPlayFunction && { hasPlayFunction: true }),
|
|
565
|
+
...(wrappedPlay && { play: wrappedPlay }),
|
|
566
|
+
...(storyId && { storyId }),
|
|
567
|
+
...(storyTags && { tags: storyTags }),
|
|
568
|
+
...(loaders.length > 0 && { loaders }),
|
|
569
|
+
...(hasArgs && { args: variantArgs }),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return variants;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Collect loaders from global, meta, and story levels
|
|
578
|
+
* Returns wrapped loader functions that execute with context
|
|
579
|
+
*/
|
|
580
|
+
function collectLoaders(
|
|
581
|
+
meta: StoryMeta,
|
|
582
|
+
story: Story | CSF2Story
|
|
583
|
+
): VariantLoader[] {
|
|
584
|
+
const allLoaders: Loader[] = [
|
|
585
|
+
...(globalPreviewConfig.loaders ?? []),
|
|
586
|
+
...(meta.loaders ?? []),
|
|
587
|
+
...(typeof story === "function" ? story.loaders ?? [] : story.loaders ?? []),
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
if (allLoaders.length === 0) {
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Wrap each loader to execute without requiring context at call time
|
|
595
|
+
// The actual context will be built when the loader is executed
|
|
596
|
+
return allLoaders.map((loader) => {
|
|
597
|
+
return async (): Promise<Record<string, unknown>> => {
|
|
598
|
+
// Create a minimal context for loader execution
|
|
599
|
+
const minimalContext: StoryContext = {
|
|
600
|
+
args: {},
|
|
601
|
+
argTypes: {},
|
|
602
|
+
globals: {},
|
|
603
|
+
parameters: {},
|
|
604
|
+
id: "",
|
|
605
|
+
kind: meta.title || "Unknown",
|
|
606
|
+
name: "",
|
|
607
|
+
story: "",
|
|
608
|
+
viewMode: "story",
|
|
609
|
+
loaded: {},
|
|
610
|
+
abortSignal: new AbortController().signal,
|
|
611
|
+
componentId: "",
|
|
612
|
+
title: meta.title || "Unknown",
|
|
613
|
+
};
|
|
614
|
+
return loader(minimalContext);
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Build a StoryContext for decorators and render functions
|
|
621
|
+
*/
|
|
622
|
+
function buildStoryContext(
|
|
623
|
+
meta: StoryMeta,
|
|
624
|
+
story: Story | CSF2Story,
|
|
625
|
+
args: Record<string, unknown>,
|
|
626
|
+
storyId: string,
|
|
627
|
+
storyName: string,
|
|
628
|
+
loadedData?: Record<string, unknown>
|
|
629
|
+
): StoryContext {
|
|
630
|
+
const mergedArgs = {
|
|
631
|
+
...globalPreviewConfig.args,
|
|
632
|
+
...meta.args,
|
|
633
|
+
...(typeof story === "object" ? story.args : story.args),
|
|
634
|
+
...args,
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const mergedArgTypes = {
|
|
638
|
+
...globalPreviewConfig.argTypes,
|
|
639
|
+
...meta.argTypes,
|
|
640
|
+
...(typeof story === "object" ? story.argTypes : story.argTypes),
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const mergedParameters = {
|
|
644
|
+
...globalPreviewConfig.parameters,
|
|
645
|
+
...meta.parameters,
|
|
646
|
+
...(typeof story === "object" ? story.parameters : story.parameters),
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
args: mergedArgs,
|
|
651
|
+
argTypes: mergedArgTypes ?? {},
|
|
652
|
+
globals: {},
|
|
653
|
+
parameters: mergedParameters ?? {},
|
|
654
|
+
id: storyId,
|
|
655
|
+
kind: meta.title || "Unknown",
|
|
656
|
+
name: storyName,
|
|
657
|
+
story: storyName,
|
|
658
|
+
viewMode: "story",
|
|
659
|
+
loaded: loadedData ?? {},
|
|
660
|
+
abortSignal: new AbortController().signal,
|
|
661
|
+
componentId: toId(meta.title || "Unknown", ""),
|
|
662
|
+
title: meta.title || "Unknown",
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Create a render function for a story
|
|
668
|
+
* Handles both CSF 3 (objects) and CSF 2 (functions)
|
|
669
|
+
* Applies decorators in correct order: story → meta → global (innermost first)
|
|
670
|
+
* Accepts optional args overrides and loaded data from loaders
|
|
671
|
+
*/
|
|
672
|
+
function createRenderFunction(
|
|
673
|
+
story: Story | CSF2Story,
|
|
674
|
+
component: ComponentType<unknown>,
|
|
675
|
+
meta: StoryMeta,
|
|
676
|
+
storyId: string,
|
|
677
|
+
storyName: string
|
|
678
|
+
): (options?: VariantRenderOptions) => ReactNode {
|
|
679
|
+
return (options?: VariantRenderOptions) => {
|
|
680
|
+
// Merge args: global → meta → story → runtime overrides
|
|
681
|
+
const args = {
|
|
682
|
+
...globalPreviewConfig.args,
|
|
683
|
+
...meta.args,
|
|
684
|
+
...(typeof story === "function" ? story.args : story.args),
|
|
685
|
+
...options?.args, // Runtime overrides from viewer props panel
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const loadedData = options?.loadedData;
|
|
689
|
+
|
|
690
|
+
// Build the story context with loaded data
|
|
691
|
+
const context = buildStoryContext(meta, story, args, storyId, storyName, loadedData);
|
|
692
|
+
|
|
693
|
+
// Create the base render function
|
|
694
|
+
let renderFn: () => ReactNode;
|
|
695
|
+
|
|
696
|
+
if (typeof story === "function") {
|
|
697
|
+
// CSF 2: Story is a function (from Template.bind({}))
|
|
698
|
+
renderFn = () => story(args);
|
|
699
|
+
} else if (story.render) {
|
|
700
|
+
// CSF 3: Story has custom render function
|
|
701
|
+
// Support both render(args) and render(args, context) signatures
|
|
702
|
+
renderFn = () =>
|
|
703
|
+
story.render!.length >= 2
|
|
704
|
+
? story.render!(args, context)
|
|
705
|
+
: story.render!(args);
|
|
706
|
+
} else if (meta.render) {
|
|
707
|
+
// CSF 3: Meta has default render function
|
|
708
|
+
renderFn = () =>
|
|
709
|
+
meta.render!.length >= 2
|
|
710
|
+
? meta.render!(args, context)
|
|
711
|
+
: meta.render!(args);
|
|
712
|
+
} else {
|
|
713
|
+
// Default: render component with args
|
|
714
|
+
renderFn = () => createElement(component, args);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Collect decorators in Storybook order
|
|
718
|
+
// story → meta → global, then reverse to apply innermost first
|
|
719
|
+
const allDecorators = [
|
|
720
|
+
...(globalPreviewConfig.decorators ?? []),
|
|
721
|
+
...(meta.decorators ?? []),
|
|
722
|
+
...(typeof story === "function" ? story.decorators ?? [] : story.decorators ?? []),
|
|
723
|
+
].reverse();
|
|
724
|
+
|
|
725
|
+
// Apply decorators if any
|
|
726
|
+
if (allDecorators.length > 0) {
|
|
727
|
+
return applyDecorators(renderFn, allDecorators, context);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return renderFn();
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Apply decorators in the correct order
|
|
736
|
+
* Decorators wrap from innermost to outermost
|
|
737
|
+
*/
|
|
738
|
+
function applyDecorators(
|
|
739
|
+
renderFn: () => ReactNode,
|
|
740
|
+
decorators: Decorator[],
|
|
741
|
+
context: StoryContext
|
|
742
|
+
): ReactNode {
|
|
743
|
+
// Start with the base render function
|
|
744
|
+
let storyFn: () => ReactNode = renderFn;
|
|
745
|
+
|
|
746
|
+
// Each decorator wraps the previous one
|
|
747
|
+
for (const decorator of decorators) {
|
|
748
|
+
const wrappedFn = storyFn;
|
|
749
|
+
storyFn = () => decorator(wrappedFn, context);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return storyFn();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Convert PascalCase to Title Case
|
|
757
|
+
* @deprecated Use storyNameFromExport from @storybook/csf instead
|
|
758
|
+
*/
|
|
759
|
+
function pascalToTitle(name: string): string {
|
|
760
|
+
return name.replace(/([A-Z])/g, " $1").trim();
|
|
761
|
+
}
|