@repobuddy/storybook 2.11.0 → 2.13.0
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/esm/index.d.ts +8 -1
- package/esm/index.js +105 -81
- package/esm/storybook-addon-tag-badges/index.d.ts +3 -2
- package/esm/storybook-addon-tag-badges/index.js +19 -5
- package/package.json +1 -1
- package/readme.md +13 -16
- package/src/contexts/story_card_registry_context.tsx +17 -0
- package/src/contexts/story_card_scope.tsx +83 -0
- package/src/decorators/show_doc_source.tsx +45 -23
- package/src/decorators/with_story_card.tsx +4 -96
- package/src/storybook-addon-tag-badges/tag_badges.ts +23 -3
package/esm/index.d.ts
CHANGED
|
@@ -99,11 +99,18 @@ declare function StoryCard({
|
|
|
99
99
|
* @param options.showOriginalSource - Whether to show the original source code in a card
|
|
100
100
|
* @param options.className - Class name to apply to the card
|
|
101
101
|
* @param options.source - Source code to show if provided.
|
|
102
|
-
* @
|
|
102
|
+
* @param options.placement - Where to show the source code relative to the story.
|
|
103
|
+
* @returns A decorator function that shows the source code of a story above or below the rendered story
|
|
103
104
|
*/
|
|
104
105
|
declare function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Args>(options?: Pick<StoryCardProps, 'className'> & {
|
|
105
106
|
source?: string | undefined;
|
|
106
107
|
showOriginalSource?: boolean | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Where to show the source code relative to the story.
|
|
110
|
+
*
|
|
111
|
+
* @default 'after'
|
|
112
|
+
*/
|
|
113
|
+
placement?: 'before' | 'after' | undefined;
|
|
107
114
|
}): DecoratorFunction<TRenderer, TArgs>;
|
|
108
115
|
//#endregion
|
|
109
116
|
//#region src/decorators/with_story_card.d.ts
|
package/esm/index.js
CHANGED
|
@@ -71,6 +71,84 @@ function StoryCard({ status, className, children, title }) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/utils/generate_key.ts
|
|
76
|
+
/**
|
|
77
|
+
* Generates a key for React collections, falling back to a simple counter-based ID if crypto.randomUUID is unavailable.
|
|
78
|
+
* crypto.randomUUID() requires a secure context (HTTPS, localhost, or 127.0.0.1).
|
|
79
|
+
*
|
|
80
|
+
* This can be moved to `@just-web` in the future.
|
|
81
|
+
*/
|
|
82
|
+
function generateKey(prefix) {
|
|
83
|
+
const randomId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
84
|
+
return prefix ? `${prefix}-${randomId}` : randomId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/contexts/story_card_registry_context.tsx
|
|
89
|
+
const StoryCardRegistryContext = createContext(null);
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/contexts/story_card_scope.tsx
|
|
93
|
+
/**
|
|
94
|
+
* Ensures a story-card collection scope: creates the root container when no context exists,
|
|
95
|
+
* otherwise renders the collector so this card participates in the existing scope.
|
|
96
|
+
*/
|
|
97
|
+
function StoryCardScope({ Story, ...props }) {
|
|
98
|
+
const context = useContext(StoryCardRegistryContext);
|
|
99
|
+
const collector = /* @__PURE__ */ jsx(StoryCardCollector, {
|
|
100
|
+
Story,
|
|
101
|
+
...props
|
|
102
|
+
});
|
|
103
|
+
if (context === null) return /* @__PURE__ */ jsx(StoryCardContainer, { children: collector });
|
|
104
|
+
return collector;
|
|
105
|
+
}
|
|
106
|
+
function StoryCardContainer({ children }) {
|
|
107
|
+
const [cards, setCards] = useState([]);
|
|
108
|
+
const contextValue = useMemo(() => ({
|
|
109
|
+
add(card) {
|
|
110
|
+
const key = generateKey("story-card");
|
|
111
|
+
setCards((cards) => [...cards, {
|
|
112
|
+
...card,
|
|
113
|
+
key
|
|
114
|
+
}]);
|
|
115
|
+
return key;
|
|
116
|
+
},
|
|
117
|
+
remove(key) {
|
|
118
|
+
setCards((cards) => cards.filter((card) => card.key !== key));
|
|
119
|
+
}
|
|
120
|
+
}), []);
|
|
121
|
+
return /* @__PURE__ */ jsx(StoryCardRegistryContext.Provider, {
|
|
122
|
+
value: contextValue,
|
|
123
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
124
|
+
className: "rbsb:flex rbsb:flex-col rbsb:gap-2",
|
|
125
|
+
children: [cards.map(({ content, key, ...rest }) => /* @__PURE__ */ jsx(StoryCard, {
|
|
126
|
+
...rest,
|
|
127
|
+
children: content
|
|
128
|
+
}, key)), children]
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function StoryCardCollector({ Story, title, status, className, content }) {
|
|
133
|
+
const context = useContext(StoryCardRegistryContext);
|
|
134
|
+
const cardIdRef = useRef(null);
|
|
135
|
+
useLayoutEffect(() => {
|
|
136
|
+
if (cardIdRef.current === null) cardIdRef.current = context.add({
|
|
137
|
+
title,
|
|
138
|
+
status,
|
|
139
|
+
className,
|
|
140
|
+
content
|
|
141
|
+
});
|
|
142
|
+
return () => {
|
|
143
|
+
if (cardIdRef.current !== null) {
|
|
144
|
+
context.remove(cardIdRef.current);
|
|
145
|
+
cardIdRef.current = null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
return /* @__PURE__ */ jsx(Story, {});
|
|
150
|
+
}
|
|
151
|
+
|
|
74
152
|
//#endregion
|
|
75
153
|
//#region src/decorators/show_doc_source.tsx
|
|
76
154
|
const channel = addons.getChannel();
|
|
@@ -82,7 +160,8 @@ const channel = addons.getChannel();
|
|
|
82
160
|
* @param options.showOriginalSource - Whether to show the original source code in a card
|
|
83
161
|
* @param options.className - Class name to apply to the card
|
|
84
162
|
* @param options.source - Source code to show if provided.
|
|
85
|
-
* @
|
|
163
|
+
* @param options.placement - Where to show the source code relative to the story.
|
|
164
|
+
* @returns A decorator function that shows the source code of a story above or below the rendered story
|
|
86
165
|
*/
|
|
87
166
|
function showDocSource(options) {
|
|
88
167
|
return (Story, { parameters: { docs, darkMode } }) => {
|
|
@@ -96,12 +175,31 @@ function showDocSource(options) {
|
|
|
96
175
|
const code = options?.source ?? (options?.showOriginalSource ? docs?.source?.originalSource : docs?.source?.code ?? docs?.source?.originalSource);
|
|
97
176
|
const language = code === docs?.source?.originalSource ? void 0 : docs?.source?.language;
|
|
98
177
|
const isOriginalSource = code === docs?.source?.originalSource;
|
|
99
|
-
const
|
|
178
|
+
const sourceContent = /* @__PURE__ */ jsx(SyntaxHighlighter, {
|
|
100
179
|
language,
|
|
101
180
|
children: code
|
|
102
181
|
});
|
|
182
|
+
const showBefore = options?.placement === "before";
|
|
183
|
+
const sourceCardClassName = (state) => {
|
|
184
|
+
const modifiedState = {
|
|
185
|
+
...state,
|
|
186
|
+
defaultClassName: twJoin(state.defaultClassName, isOriginalSource ? "rbsb:bg-transparent rbsb:dark:bg-transparent" : "rbsb:bg-gray-100 rbsb:dark:bg-gray-900")
|
|
187
|
+
};
|
|
188
|
+
const className = options?.className;
|
|
189
|
+
return typeof className === "function" ? className(modifiedState) : twJoin(modifiedState.defaultClassName, className);
|
|
190
|
+
};
|
|
191
|
+
const theme = convert(docs?.theme ?? (isDark ? themes.dark : themes.light));
|
|
192
|
+
if (showBefore) return /* @__PURE__ */ jsx(ThemeProvider, {
|
|
193
|
+
theme,
|
|
194
|
+
children: /* @__PURE__ */ jsx(StoryCardScope, {
|
|
195
|
+
Story,
|
|
196
|
+
content: sourceContent,
|
|
197
|
+
className: sourceCardClassName,
|
|
198
|
+
status: "info"
|
|
199
|
+
})
|
|
200
|
+
});
|
|
103
201
|
return /* @__PURE__ */ jsx(ThemeProvider, {
|
|
104
|
-
theme
|
|
202
|
+
theme,
|
|
105
203
|
children: /* @__PURE__ */ jsxs("section", {
|
|
106
204
|
style: {
|
|
107
205
|
display: "flex",
|
|
@@ -109,34 +207,15 @@ function showDocSource(options) {
|
|
|
109
207
|
gap: "1rem"
|
|
110
208
|
},
|
|
111
209
|
children: [/* @__PURE__ */ jsx(Story, {}), /* @__PURE__ */ jsx(StoryCard, {
|
|
112
|
-
className:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
defaultClassName: twJoin(state.defaultClassName, isOriginalSource ? "rbsb:bg-transparent rbsb:dark:bg-transparent" : "rbsb:bg-gray-100 rbsb:dark:bg-gray-900")
|
|
116
|
-
};
|
|
117
|
-
const className = options?.className;
|
|
118
|
-
return typeof className === "function" ? className(modifiedState) : twJoin(modifiedState.defaultClassName, className);
|
|
119
|
-
},
|
|
120
|
-
children: content
|
|
210
|
+
className: sourceCardClassName,
|
|
211
|
+
status: "info",
|
|
212
|
+
children: sourceContent
|
|
121
213
|
})]
|
|
122
214
|
})
|
|
123
215
|
});
|
|
124
216
|
};
|
|
125
217
|
}
|
|
126
218
|
|
|
127
|
-
//#endregion
|
|
128
|
-
//#region src/utils/generate_key.ts
|
|
129
|
-
/**
|
|
130
|
-
* Generates a key for React collections, falling back to a simple counter-based ID if crypto.randomUUID is unavailable.
|
|
131
|
-
* crypto.randomUUID() requires a secure context (HTTPS, localhost, or 127.0.0.1).
|
|
132
|
-
*
|
|
133
|
-
* This can be moved to `@just-web` in the future.
|
|
134
|
-
*/
|
|
135
|
-
function generateKey(prefix) {
|
|
136
|
-
const randomId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
137
|
-
return prefix ? `${prefix}-${randomId}` : randomId;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
219
|
//#endregion
|
|
141
220
|
//#region src/decorators/with_story_card.tsx
|
|
142
221
|
/**
|
|
@@ -227,7 +306,7 @@ function withStoryCard({ title, status, content: contentProp, className, ...rest
|
|
|
227
306
|
const finalClassName = className ?? storyCardParam?.className;
|
|
228
307
|
const content = finalContent ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component;
|
|
229
308
|
if (!content && !finalTitle) return /* @__PURE__ */ jsx(Story, {});
|
|
230
|
-
return /* @__PURE__ */ jsx(
|
|
309
|
+
return /* @__PURE__ */ jsx(StoryCardScope, {
|
|
231
310
|
Story,
|
|
232
311
|
content,
|
|
233
312
|
title: finalTitle,
|
|
@@ -237,61 +316,6 @@ function withStoryCard({ title, status, content: contentProp, className, ...rest
|
|
|
237
316
|
});
|
|
238
317
|
};
|
|
239
318
|
}
|
|
240
|
-
function StoryCardContainerWrapper({ Story, ...props }) {
|
|
241
|
-
const context = useContext(StoryCardContext);
|
|
242
|
-
const collector = /* @__PURE__ */ jsx(StoryCardCollector, {
|
|
243
|
-
Story,
|
|
244
|
-
...props
|
|
245
|
-
});
|
|
246
|
-
if (context === null) return /* @__PURE__ */ jsx(StoryCardContainer, { children: collector });
|
|
247
|
-
return collector;
|
|
248
|
-
}
|
|
249
|
-
function StoryCardContainer({ children }) {
|
|
250
|
-
const [cards, setCards] = useState([]);
|
|
251
|
-
const contextValue = useMemo(() => ({
|
|
252
|
-
addCard(card) {
|
|
253
|
-
const key = generateKey("story-card");
|
|
254
|
-
setCards((cards) => [...cards, {
|
|
255
|
-
...card,
|
|
256
|
-
key
|
|
257
|
-
}]);
|
|
258
|
-
return key;
|
|
259
|
-
},
|
|
260
|
-
removeCard(key) {
|
|
261
|
-
setCards((cards) => cards.filter((card) => card.key !== key));
|
|
262
|
-
}
|
|
263
|
-
}), []);
|
|
264
|
-
return /* @__PURE__ */ jsx(StoryCardContext.Provider, {
|
|
265
|
-
value: contextValue,
|
|
266
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
267
|
-
className: "rbsb:flex rbsb:flex-col rbsb:gap-2",
|
|
268
|
-
children: [cards.map(({ content, key, ...rest }) => /* @__PURE__ */ jsx(StoryCard, {
|
|
269
|
-
...rest,
|
|
270
|
-
children: content
|
|
271
|
-
}, key)), children]
|
|
272
|
-
})
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
function StoryCardCollector({ Story, title, status, className, content }) {
|
|
276
|
-
const context = useContext(StoryCardContext);
|
|
277
|
-
const cardIdRef = useRef(null);
|
|
278
|
-
useLayoutEffect(() => {
|
|
279
|
-
if (cardIdRef.current === null) cardIdRef.current = context.addCard({
|
|
280
|
-
title,
|
|
281
|
-
status,
|
|
282
|
-
className,
|
|
283
|
-
content
|
|
284
|
-
});
|
|
285
|
-
return () => {
|
|
286
|
-
if (cardIdRef.current !== null) {
|
|
287
|
-
context.removeCard(cardIdRef.current);
|
|
288
|
-
cardIdRef.current = null;
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}, []);
|
|
292
|
-
return /* @__PURE__ */ jsx(Story, {});
|
|
293
|
-
}
|
|
294
|
-
const StoryCardContext = createContext(null);
|
|
295
319
|
|
|
296
320
|
//#endregion
|
|
297
321
|
//#region src/parameters/define_actions_param.ts
|
|
@@ -8,7 +8,7 @@ type TagBadgeParameter = TagBadgeParameters[0];
|
|
|
8
8
|
/**
|
|
9
9
|
* Type representing the names of predefined tags used in Storybook stories.
|
|
10
10
|
*/
|
|
11
|
-
type TagNames = 'editor' | 'new' | 'beta' | 'props' | 'deprecated' | 'outdated' | 'danger' | 'todo' | 'code-only' | 'snapshot' | 'unit' | 'integration' | 'keyboard' | 'internal' | 'usecase' | 'version:next';
|
|
11
|
+
type TagNames = 'editor' | 'source' | 'new' | 'beta' | 'props' | 'deprecated' | 'outdated' | 'danger' | 'todo' | 'code-only' | 'snapshot' | 'unit' | 'integration' | 'keyboard' | 'internal' | 'usecase' | 'version:next';
|
|
12
12
|
/**
|
|
13
13
|
* Configuration for story tag badges that appear in the Storybook sidebar.
|
|
14
14
|
* Each badge is associated with a specific tag and displays an emoji with a tooltip.
|
|
@@ -37,6 +37,7 @@ declare const outdatedBadge: TagBadgeParameter;
|
|
|
37
37
|
declare const dangerBadge: TagBadgeParameter;
|
|
38
38
|
declare const todoBadge: TagBadgeParameter;
|
|
39
39
|
declare const codeOnlyBadge: TagBadgeParameter;
|
|
40
|
+
declare const sourceBadge: TagBadgeParameter;
|
|
40
41
|
declare const snapshotBadge: TagBadgeParameter;
|
|
41
42
|
declare const unitBadge: TagBadgeParameter;
|
|
42
43
|
declare const integrationBadge: TagBadgeParameter;
|
|
@@ -115,4 +116,4 @@ type StoryObj<TMetaOrCmpOrArgs = Args> = ExtendStoryObj<TMetaOrCmpOrArgs, StoryO
|
|
|
115
116
|
tag: TagNames;
|
|
116
117
|
}>;
|
|
117
118
|
//#endregion
|
|
118
|
-
export { Meta, StoryObj, TagNames, betaBadge, codeOnlyBadge, dangerBadge, deprecatedBadge, editorBadge, integrationBadge, internalBadge, keyboardBadge, newBadge, outdatedBadge, propsBadge, snapshotBadge, tagBadges, todoBadge, unitBadge };
|
|
119
|
+
export { Meta, StoryObj, TagNames, betaBadge, codeOnlyBadge, dangerBadge, deprecatedBadge, editorBadge, integrationBadge, internalBadge, keyboardBadge, newBadge, outdatedBadge, propsBadge, snapshotBadge, sourceBadge, tagBadges, todoBadge, unitBadge };
|
|
@@ -123,7 +123,20 @@ const codeOnlyBadge = {
|
|
|
123
123
|
borderColor: "transparent"
|
|
124
124
|
},
|
|
125
125
|
tooltip: "Code Only"
|
|
126
|
-
}
|
|
126
|
+
},
|
|
127
|
+
display: { mdx: false }
|
|
128
|
+
};
|
|
129
|
+
const sourceBadge = {
|
|
130
|
+
tags: "source",
|
|
131
|
+
badge: {
|
|
132
|
+
text: "</>",
|
|
133
|
+
style: {
|
|
134
|
+
backgroundColor: "transparent",
|
|
135
|
+
borderColor: "transparent"
|
|
136
|
+
},
|
|
137
|
+
tooltip: "Source Code"
|
|
138
|
+
},
|
|
139
|
+
display: { mdx: false }
|
|
127
140
|
};
|
|
128
141
|
const snapshotBadge = {
|
|
129
142
|
tags: "snapshot",
|
|
@@ -150,7 +163,7 @@ const unitBadge = {
|
|
|
150
163
|
},
|
|
151
164
|
tooltip: "Unit Test"
|
|
152
165
|
},
|
|
153
|
-
display: { sidebar:
|
|
166
|
+
display: { sidebar: true }
|
|
154
167
|
};
|
|
155
168
|
const integrationBadge = {
|
|
156
169
|
tags: "integration",
|
|
@@ -188,6 +201,7 @@ const internalBadge = {
|
|
|
188
201
|
};
|
|
189
202
|
const tagBadges = [
|
|
190
203
|
editorBadge,
|
|
204
|
+
sourceBadge,
|
|
191
205
|
unitBadge,
|
|
192
206
|
integrationBadge,
|
|
193
207
|
keyboardBadge,
|
|
@@ -199,10 +213,10 @@ const tagBadges = [
|
|
|
199
213
|
propsBadge,
|
|
200
214
|
todoBadge,
|
|
201
215
|
codeOnlyBadge,
|
|
216
|
+
versionBadge,
|
|
202
217
|
internalBadge,
|
|
203
|
-
snapshotBadge
|
|
204
|
-
versionBadge
|
|
218
|
+
snapshotBadge
|
|
205
219
|
];
|
|
206
220
|
|
|
207
221
|
//#endregion
|
|
208
|
-
export { betaBadge, codeOnlyBadge, dangerBadge, deprecatedBadge, editorBadge, integrationBadge, internalBadge, keyboardBadge, newBadge, outdatedBadge, propsBadge, snapshotBadge, tagBadges, todoBadge, unitBadge };
|
|
222
|
+
export { betaBadge, codeOnlyBadge, dangerBadge, deprecatedBadge, editorBadge, integrationBadge, internalBadge, keyboardBadge, newBadge, outdatedBadge, propsBadge, snapshotBadge, sourceBadge, tagBadges, todoBadge, unitBadge };
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -139,6 +139,9 @@ export const preview: Preview = {
|
|
|
139
139
|
|
|
140
140
|
[`@repobuddy/storybook`][`@repobuddy/storybook`] uses Tailwind CSS 4 with the prefix `rbsb:` and support `dark` variant.
|
|
141
141
|
|
|
142
|
+
While we provide a pre-built `@repobuddy/storybook/styles.css` for you,
|
|
143
|
+
it uses the default `dark` variant which is based on the `(prefers-color-scheme: dark)` media query.
|
|
144
|
+
|
|
142
145
|
Since how to control the dark variant is different in different projects,
|
|
143
146
|
the pre-built `@repobuddy/storybook/styles.css` might not work for you.
|
|
144
147
|
|
|
@@ -149,38 +152,32 @@ you need to separate your tailwind config and import them in your storybook.
|
|
|
149
152
|
|
|
150
153
|
```tsx
|
|
151
154
|
// .storybook/preview.tsx
|
|
152
|
-
import './tailwind.css'
|
|
153
|
-
import './tailwind.repobuddy-storybook.css'
|
|
154
155
|
import '../tailwind.css'
|
|
156
|
+
import './tailwind.repobuddy-storybook.css'
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
```css
|
|
158
|
-
/*
|
|
160
|
+
/* tailwind.css */
|
|
161
|
+
/* add `@repobuddy/storybook/tailwind.css` to a separate layer "repobuddy-storybook" to control the layer order */
|
|
162
|
+
@layer theme, base, repobuddy-storybook, components, utilities;
|
|
163
|
+
@import "tailwindcss";
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
@layer theme, base, components, rbsb, utilities;
|
|
162
|
-
@import "tailwindcss/preflight.css" layer(base);
|
|
165
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
163
166
|
```
|
|
164
167
|
|
|
165
168
|
```css
|
|
166
169
|
/* .storybook/tailwind.repobuddy-storybook.css */
|
|
167
|
-
|
|
168
|
-
@import "@repobuddy/storybook/tailwind.css" layer(rbsb);
|
|
170
|
+
@import "@repobuddy/storybook/tailwind.css" layer(repobuddy-storybook);
|
|
169
171
|
|
|
170
172
|
@source "../node_modules/@repobuddy/storybook/src/**";
|
|
171
173
|
|
|
172
174
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@import "tailwindcss/theme.css" layer(theme) prefix(app);
|
|
178
|
-
@import "tailwindcss/utilities.css" layer(utilities) prefix(app);
|
|
179
|
-
|
|
180
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
181
|
-
```
|
|
177
|
+
You may notice that the `@custom-variant dark` is duplicated in both files.
|
|
178
|
+
If you want to avoid this, you can extract it to a separate file and import it in both files.
|
|
182
179
|
|
|
183
|
-
|
|
180
|
+
Also note that `@repobuddy/storybook/tailwind` is deprecated in favor of `@repobuddy/storybook/tailwind.css`.
|
|
184
181
|
That convention better aligns with the Tailwind CSS 4 convention.
|
|
185
182
|
|
|
186
183
|
[`@repobuddy/storybook`]: https://github.com/repobuddy/storybook
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { createContext } from 'react'
|
|
3
|
+
import type { RequiredPick } from 'type-plus'
|
|
4
|
+
import type { StoryCardProps } from '../components/story_card.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Payload for adding a card to the story card registry.
|
|
8
|
+
* Matches the shape of card props with `status` required.
|
|
9
|
+
*/
|
|
10
|
+
export type StoryCardEntry = RequiredPick<Omit<StoryCardProps, 'children'> & { content?: ReactNode }, 'status'>
|
|
11
|
+
|
|
12
|
+
export interface StoryCardRegistryContextValue {
|
|
13
|
+
add: (card: StoryCardEntry) => string
|
|
14
|
+
remove: (id: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StoryCardRegistryContext = createContext<StoryCardRegistryContextValue | null>(null)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { StoryCard } from '../components/story_card.js'
|
|
3
|
+
import { generateKey } from '../utils/generate_key.js'
|
|
4
|
+
import {
|
|
5
|
+
type StoryCardEntry,
|
|
6
|
+
StoryCardRegistryContext,
|
|
7
|
+
type StoryCardRegistryContextValue
|
|
8
|
+
} from './story_card_registry_context.js'
|
|
9
|
+
|
|
10
|
+
export type StoryCardScopeProps = { Story: ComponentType } & StoryCardEntry
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensures a story-card collection scope: creates the root container when no context exists,
|
|
14
|
+
* otherwise renders the collector so this card participates in the existing scope.
|
|
15
|
+
*/
|
|
16
|
+
export function StoryCardScope({ Story, ...props }: StoryCardScopeProps) {
|
|
17
|
+
const context = useContext(StoryCardRegistryContext)
|
|
18
|
+
const collector = <StoryCardCollector Story={Story} {...props} />
|
|
19
|
+
|
|
20
|
+
if (context === null) {
|
|
21
|
+
return <StoryCardContainer>{collector}</StoryCardContainer>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return collector
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function StoryCardContainer({ children }: { children: ReactNode }) {
|
|
28
|
+
const [cards, setCards] = useState<StoryCardEntryWithKey[]>([])
|
|
29
|
+
|
|
30
|
+
const contextValue: StoryCardRegistryContextValue = useMemo(
|
|
31
|
+
() => ({
|
|
32
|
+
add(card) {
|
|
33
|
+
const key = generateKey('story-card')
|
|
34
|
+
setCards((cards) => [...cards, { ...card, key }])
|
|
35
|
+
return key
|
|
36
|
+
},
|
|
37
|
+
remove(key) {
|
|
38
|
+
setCards((cards) => cards.filter((card) => card.key !== key))
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
[]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<StoryCardRegistryContext.Provider value={contextValue}>
|
|
46
|
+
<div className="rbsb:flex rbsb:flex-col rbsb:gap-2">
|
|
47
|
+
{cards.map(({ content, key, ...rest }) => (
|
|
48
|
+
<StoryCard key={key} {...rest}>
|
|
49
|
+
{content}
|
|
50
|
+
</StoryCard>
|
|
51
|
+
))}
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
</StoryCardRegistryContext.Provider>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type StoryCardEntryWithKey = StoryCardEntry & { key: string }
|
|
59
|
+
|
|
60
|
+
interface StoryCardCollectorProps extends StoryCardScopeProps {}
|
|
61
|
+
|
|
62
|
+
function StoryCardCollector({ Story, title, status, className, content }: StoryCardCollectorProps) {
|
|
63
|
+
// StoryCardCollector is an internal component. Context is guaranteed to be not null by `StoryCardContainer`.
|
|
64
|
+
const context = useContext(StoryCardRegistryContext)!
|
|
65
|
+
const cardIdRef = useRef<string | null>(null)
|
|
66
|
+
|
|
67
|
+
// Collect this card once into the collection
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
// Only add if not already added (handles Strict Mode double-render)
|
|
70
|
+
if (cardIdRef.current === null) {
|
|
71
|
+
cardIdRef.current = context.add({ title, status, className, content })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
if (cardIdRef.current !== null) {
|
|
76
|
+
context.remove(cardIdRef.current)
|
|
77
|
+
cardIdRef.current = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
return <Story />
|
|
83
|
+
}
|
|
@@ -5,6 +5,7 @@ import { addons } from 'storybook/preview-api'
|
|
|
5
5
|
import { convert, ThemeProvider, themes } from 'storybook/theming'
|
|
6
6
|
import { twJoin } from 'tailwind-merge'
|
|
7
7
|
import { StoryCard, type StoryCardProps } from '../components/story_card'
|
|
8
|
+
import { StoryCardScope } from '../contexts/story_card_scope'
|
|
8
9
|
|
|
9
10
|
const channel = addons.getChannel()
|
|
10
11
|
|
|
@@ -16,12 +17,19 @@ const channel = addons.getChannel()
|
|
|
16
17
|
* @param options.showOriginalSource - Whether to show the original source code in a card
|
|
17
18
|
* @param options.className - Class name to apply to the card
|
|
18
19
|
* @param options.source - Source code to show if provided.
|
|
19
|
-
* @
|
|
20
|
+
* @param options.placement - Where to show the source code relative to the story.
|
|
21
|
+
* @returns A decorator function that shows the source code of a story above or below the rendered story
|
|
20
22
|
*/
|
|
21
23
|
export function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Args>(
|
|
22
24
|
options?: Pick<StoryCardProps, 'className'> & {
|
|
23
25
|
source?: string | undefined
|
|
24
26
|
showOriginalSource?: boolean | undefined
|
|
27
|
+
/**
|
|
28
|
+
* Where to show the source code relative to the story.
|
|
29
|
+
*
|
|
30
|
+
* @default 'after'
|
|
31
|
+
*/
|
|
32
|
+
placement?: 'before' | 'after' | undefined
|
|
25
33
|
}
|
|
26
34
|
): DecoratorFunction<TRenderer, TArgs> {
|
|
27
35
|
return (Story, { parameters: { docs, darkMode } }) => {
|
|
@@ -46,10 +54,43 @@ export function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Arg
|
|
|
46
54
|
|
|
47
55
|
const isOriginalSource = code === docs?.source?.originalSource
|
|
48
56
|
|
|
49
|
-
const
|
|
57
|
+
const sourceContent = <SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
|
|
58
|
+
|
|
59
|
+
const showBefore = options?.placement === 'before'
|
|
60
|
+
|
|
61
|
+
const sourceCardClassName = (state: Pick<StoryCardProps, 'status'> & { defaultClassName: string }) => {
|
|
62
|
+
const modifiedState = {
|
|
63
|
+
...state,
|
|
64
|
+
defaultClassName: twJoin(
|
|
65
|
+
state.defaultClassName,
|
|
66
|
+
isOriginalSource ? 'rbsb:bg-transparent rbsb:dark:bg-transparent' : 'rbsb:bg-gray-100 rbsb:dark:bg-gray-900'
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const className = options?.className
|
|
71
|
+
return typeof className === 'function'
|
|
72
|
+
? className(modifiedState)
|
|
73
|
+
: twJoin(modifiedState.defaultClassName, className)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const theme = convert(docs?.theme ?? (isDark ? themes.dark : themes.light))
|
|
77
|
+
|
|
78
|
+
if (showBefore) {
|
|
79
|
+
return (
|
|
80
|
+
<ThemeProvider theme={theme}>
|
|
81
|
+
<StoryCardScope Story={Story} content={sourceContent} className={sourceCardClassName} status="info" />
|
|
82
|
+
</ThemeProvider>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const storyCard = (
|
|
87
|
+
<StoryCard className={sourceCardClassName} status="info">
|
|
88
|
+
{sourceContent}
|
|
89
|
+
</StoryCard>
|
|
90
|
+
)
|
|
50
91
|
|
|
51
92
|
return (
|
|
52
|
-
<ThemeProvider theme={
|
|
93
|
+
<ThemeProvider theme={theme}>
|
|
53
94
|
<section
|
|
54
95
|
style={{
|
|
55
96
|
display: 'flex',
|
|
@@ -58,26 +99,7 @@ export function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Arg
|
|
|
58
99
|
}}
|
|
59
100
|
>
|
|
60
101
|
<Story />
|
|
61
|
-
|
|
62
|
-
className={(state) => {
|
|
63
|
-
const modifiedState = {
|
|
64
|
-
...state,
|
|
65
|
-
defaultClassName: twJoin(
|
|
66
|
-
state.defaultClassName,
|
|
67
|
-
isOriginalSource
|
|
68
|
-
? 'rbsb:bg-transparent rbsb:dark:bg-transparent'
|
|
69
|
-
: 'rbsb:bg-gray-100 rbsb:dark:bg-gray-900'
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const className = options?.className
|
|
74
|
-
return typeof className === 'function'
|
|
75
|
-
? className(modifiedState)
|
|
76
|
-
: twJoin(modifiedState.defaultClassName, className)
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
{content}
|
|
80
|
-
</StoryCard>
|
|
102
|
+
{storyCard}
|
|
81
103
|
</section>
|
|
82
104
|
</ThemeProvider>
|
|
83
105
|
)
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ComponentType,
|
|
3
|
-
createContext,
|
|
4
|
-
type ReactNode,
|
|
5
|
-
useContext,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState
|
|
10
|
-
} from 'react'
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
11
2
|
import type { DecoratorFunction, Renderer } from 'storybook/internal/csf'
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
3
|
+
import type { StoryCardProps } from '../components/story_card.js'
|
|
4
|
+
import { StoryCardScope } from '../contexts/story_card_scope.js'
|
|
14
5
|
import type { StoryCardParam } from '../parameters/define_story_card_param.js'
|
|
15
|
-
import { generateKey } from '../utils/generate_key.js'
|
|
16
6
|
|
|
17
7
|
export type WithStoryCardProps = Omit<StoryCardProps, 'children' | 'className'> & {
|
|
18
8
|
/**
|
|
@@ -137,7 +127,7 @@ export function withStoryCard<TRenderer extends Renderer = Renderer>({
|
|
|
137
127
|
if (!content && !finalTitle) return <Story />
|
|
138
128
|
|
|
139
129
|
return (
|
|
140
|
-
<
|
|
130
|
+
<StoryCardScope
|
|
141
131
|
Story={Story}
|
|
142
132
|
content={content}
|
|
143
133
|
title={finalTitle}
|
|
@@ -148,85 +138,3 @@ export function withStoryCard<TRenderer extends Renderer = Renderer>({
|
|
|
148
138
|
)
|
|
149
139
|
}
|
|
150
140
|
}
|
|
151
|
-
|
|
152
|
-
interface StoryCardContainerWrapperProps extends RequiredPick<WithStoryCardProps, 'status'> {
|
|
153
|
-
Story: ComponentType
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function StoryCardContainerWrapper({ Story, ...props }: StoryCardContainerWrapperProps) {
|
|
157
|
-
const context = useContext(StoryCardContext)
|
|
158
|
-
const collector = <StoryCardCollector Story={Story} {...props} />
|
|
159
|
-
|
|
160
|
-
if (context === null) {
|
|
161
|
-
return <StoryCardContainer>{collector}</StoryCardContainer>
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return collector
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function StoryCardContainer({ children }: { children: ReactNode }) {
|
|
168
|
-
const [cards, setCards] = useState<StoryCardWithKey[]>([])
|
|
169
|
-
|
|
170
|
-
const contextValue: StoryCardContextValue = useMemo(
|
|
171
|
-
() => ({
|
|
172
|
-
addCard(card) {
|
|
173
|
-
const key = generateKey('story-card')
|
|
174
|
-
setCards((cards) => [...cards, { ...card, key }])
|
|
175
|
-
return key
|
|
176
|
-
},
|
|
177
|
-
removeCard(key) {
|
|
178
|
-
setCards((cards) => cards.filter((card) => card.key !== key))
|
|
179
|
-
}
|
|
180
|
-
}),
|
|
181
|
-
[]
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<StoryCardContext.Provider value={contextValue}>
|
|
186
|
-
<div className="rbsb:flex rbsb:flex-col rbsb:gap-2">
|
|
187
|
-
{cards.map(({ content, key, ...rest }) => (
|
|
188
|
-
<StoryCard key={key} {...rest}>
|
|
189
|
-
{content}
|
|
190
|
-
</StoryCard>
|
|
191
|
-
))}
|
|
192
|
-
{children}
|
|
193
|
-
</div>
|
|
194
|
-
</StoryCardContext.Provider>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
type StoryCardWithKey = RequiredPick<WithStoryCardProps, 'status'> & { key: string }
|
|
199
|
-
|
|
200
|
-
interface StoryCardCollectorProps extends RequiredPick<WithStoryCardProps, 'status'> {
|
|
201
|
-
Story: ComponentType
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function StoryCardCollector({ Story, title, status, className, content }: StoryCardCollectorProps) {
|
|
205
|
-
// StoryCardCollector is an internal component. Context is guaranteed to be not null by `StoryCardContainer`.
|
|
206
|
-
const context = useContext(StoryCardContext)!
|
|
207
|
-
const cardIdRef = useRef<string | null>(null)
|
|
208
|
-
|
|
209
|
-
// Collect this card once into the collection
|
|
210
|
-
useLayoutEffect(() => {
|
|
211
|
-
// Only add if not already added (handles Strict Mode double-render)
|
|
212
|
-
if (cardIdRef.current === null) {
|
|
213
|
-
cardIdRef.current = context.addCard({ title, status, className, content })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return () => {
|
|
217
|
-
if (cardIdRef.current !== null) {
|
|
218
|
-
context.removeCard(cardIdRef.current)
|
|
219
|
-
cardIdRef.current = null
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}, [])
|
|
223
|
-
|
|
224
|
-
return <Story />
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
interface StoryCardContextValue {
|
|
228
|
-
addCard: (card: RequiredPick<WithStoryCardProps, 'status'>) => string
|
|
229
|
-
removeCard: (id: string) => void
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const StoryCardContext = createContext<StoryCardContextValue | null>(null)
|
|
@@ -9,6 +9,7 @@ type TagBadgeParameter = TagBadgeParameters[0]
|
|
|
9
9
|
*/
|
|
10
10
|
export type TagNames =
|
|
11
11
|
| 'editor'
|
|
12
|
+
| 'source'
|
|
12
13
|
| 'new'
|
|
13
14
|
| 'beta'
|
|
14
15
|
| 'props'
|
|
@@ -155,6 +156,24 @@ export const codeOnlyBadge: TagBadgeParameter = {
|
|
|
155
156
|
borderColor: 'transparent'
|
|
156
157
|
},
|
|
157
158
|
tooltip: 'Code Only'
|
|
159
|
+
},
|
|
160
|
+
display: {
|
|
161
|
+
mdx: false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const sourceBadge: TagBadgeParameter = {
|
|
166
|
+
tags: 'source',
|
|
167
|
+
badge: {
|
|
168
|
+
text: '</>',
|
|
169
|
+
style: {
|
|
170
|
+
backgroundColor: 'transparent',
|
|
171
|
+
borderColor: 'transparent'
|
|
172
|
+
},
|
|
173
|
+
tooltip: 'Source Code'
|
|
174
|
+
},
|
|
175
|
+
display: {
|
|
176
|
+
mdx: false
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
|
|
@@ -185,7 +204,7 @@ export const unitBadge: TagBadgeParameter = {
|
|
|
185
204
|
tooltip: 'Unit Test'
|
|
186
205
|
},
|
|
187
206
|
display: {
|
|
188
|
-
sidebar:
|
|
207
|
+
sidebar: true
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
|
|
@@ -230,6 +249,7 @@ export const internalBadge: TagBadgeParameter = {
|
|
|
230
249
|
|
|
231
250
|
export const tagBadges: TagBadgeParameters = [
|
|
232
251
|
editorBadge,
|
|
252
|
+
sourceBadge,
|
|
233
253
|
unitBadge,
|
|
234
254
|
integrationBadge,
|
|
235
255
|
keyboardBadge,
|
|
@@ -241,7 +261,7 @@ export const tagBadges: TagBadgeParameters = [
|
|
|
241
261
|
propsBadge,
|
|
242
262
|
todoBadge,
|
|
243
263
|
codeOnlyBadge,
|
|
264
|
+
versionBadge,
|
|
244
265
|
internalBadge,
|
|
245
|
-
snapshotBadge
|
|
246
|
-
versionBadge
|
|
266
|
+
snapshotBadge
|
|
247
267
|
]
|