@repobuddy/storybook 2.10.0 → 2.12.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.js +3 -3
- package/package.json +4 -1
- package/readme.md +35 -5
- 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 +3 -3
- package/tailwind.css +1 -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
|
|
@@ -150,7 +150,7 @@ const unitBadge = {
|
|
|
150
150
|
},
|
|
151
151
|
tooltip: "Unit Test"
|
|
152
152
|
},
|
|
153
|
-
display: { sidebar:
|
|
153
|
+
display: { sidebar: true }
|
|
154
154
|
};
|
|
155
155
|
const integrationBadge = {
|
|
156
156
|
tags: "integration",
|
|
@@ -199,9 +199,9 @@ const tagBadges = [
|
|
|
199
199
|
propsBadge,
|
|
200
200
|
todoBadge,
|
|
201
201
|
codeOnlyBadge,
|
|
202
|
+
versionBadge,
|
|
202
203
|
internalBadge,
|
|
203
|
-
snapshotBadge
|
|
204
|
-
versionBadge
|
|
204
|
+
snapshotBadge
|
|
205
205
|
];
|
|
206
206
|
|
|
207
207
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repobuddy/storybook",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Storybook repo buddy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storybook",
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"./tailwind": {
|
|
43
43
|
"style": "./tailwind.css"
|
|
44
44
|
},
|
|
45
|
+
"./tailwind.css": {
|
|
46
|
+
"style": "./tailwind.css"
|
|
47
|
+
},
|
|
45
48
|
"./styles.css": "./styles.css"
|
|
46
49
|
},
|
|
47
50
|
"files": [
|
package/readme.md
CHANGED
|
@@ -135,21 +135,51 @@ export const preview: Preview = {
|
|
|
135
135
|
}
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
##
|
|
138
|
+
## Styling
|
|
139
139
|
|
|
140
|
-
[`@repobuddy/storybook`][`@repobuddy/storybook`] uses Tailwind CSS 4
|
|
140
|
+
[`@repobuddy/storybook`][`@repobuddy/storybook`] uses Tailwind CSS 4 with the prefix `rbsb:` and support `dark` variant.
|
|
141
141
|
|
|
142
|
-
|
|
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
|
+
|
|
145
|
+
Since how to control the dark variant is different in different projects,
|
|
146
|
+
the pre-built `@repobuddy/storybook/styles.css` might not work for you.
|
|
147
|
+
|
|
148
|
+
Instead, you can use the `@repobuddy/storybook/tailwind.css` to build the stylesheet for your project.
|
|
149
|
+
|
|
150
|
+
Let's say you are using it in your storybook (obviously),
|
|
151
|
+
you need to separate your tailwind config and import them in your storybook.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// .storybook/preview.tsx
|
|
155
|
+
import '../tailwind.css'
|
|
156
|
+
import './tailwind.repobuddy-storybook.css'
|
|
157
|
+
```
|
|
143
158
|
|
|
144
159
|
```css
|
|
145
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;
|
|
146
163
|
@import "tailwindcss";
|
|
147
|
-
@import "@repobuddy/storybook/tailwind";
|
|
148
164
|
|
|
149
|
-
/* specify your dark variant mechanism */
|
|
150
165
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
151
166
|
```
|
|
152
167
|
|
|
168
|
+
```css
|
|
169
|
+
/* .storybook/tailwind.repobuddy-storybook.css */
|
|
170
|
+
@import "@repobuddy/storybook/tailwind.css" layer(repobuddy-storybook);
|
|
171
|
+
|
|
172
|
+
@source "../node_modules/@repobuddy/storybook/src/**";
|
|
173
|
+
|
|
174
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
175
|
+
```
|
|
176
|
+
|
|
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.
|
|
179
|
+
|
|
180
|
+
Also note that `@repobuddy/storybook/tailwind` is deprecated in favor of `@repobuddy/storybook/tailwind.css`.
|
|
181
|
+
That convention better aligns with the Tailwind CSS 4 convention.
|
|
182
|
+
|
|
153
183
|
[`@repobuddy/storybook`]: https://github.com/repobuddy/storybook
|
|
154
184
|
[`storybook-addon-tag-badges`]: https://github.com/Sidnioulz/storybook-addon-tag-badges
|
|
155
185
|
[`@storybook-community/storybook-dark-mode`]: https://github.com/repobuddy/@storybook-community/storybook-dark-mode
|
|
@@ -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)
|
|
@@ -185,7 +185,7 @@ export const unitBadge: TagBadgeParameter = {
|
|
|
185
185
|
tooltip: 'Unit Test'
|
|
186
186
|
},
|
|
187
187
|
display: {
|
|
188
|
-
sidebar:
|
|
188
|
+
sidebar: true
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -241,7 +241,7 @@ export const tagBadges: TagBadgeParameters = [
|
|
|
241
241
|
propsBadge,
|
|
242
242
|
todoBadge,
|
|
243
243
|
codeOnlyBadge,
|
|
244
|
+
versionBadge,
|
|
244
245
|
internalBadge,
|
|
245
|
-
snapshotBadge
|
|
246
|
-
versionBadge
|
|
246
|
+
snapshotBadge
|
|
247
247
|
]
|
package/tailwind.css
CHANGED