@repobuddy/storybook 2.11.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 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
- * @returns A decorator function that shows the source code of a story above the rendered story
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
- * @returns A decorator function that shows the source code of a story above the rendered story
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 content = /* @__PURE__ */ jsx(SyntaxHighlighter, {
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: convert(docs?.theme ?? (isDark ? themes.dark : themes.light)),
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: (state) => {
113
- const modifiedState = {
114
- ...state,
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(StoryCardContainerWrapper, {
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: false }
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.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Storybook repo buddy",
5
5
  "keywords": [
6
6
  "storybook",
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
- /* .storybook/tailwind.css */
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
- /* adding the layer "rbsb" is optional */
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
- /* adding the layer "rbsb" is optional */
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
- ```css
176
- /* tailwind.css */
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
- Note that `@repobuddy/storybook/tailwind` is deprecated in favor of `@repobuddy/storybook/tailwind.css`.
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
- * @returns A decorator function that shows the source code of a story above the rendered story
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 content = <SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
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={convert(docs?.theme ?? (isDark ? themes.dark : themes.light))}>
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
- <StoryCard
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 { RequiredPick } from 'type-plus'
13
- import { StoryCard, type StoryCardProps } from '../components/story_card.js'
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
- <StoryCardContainerWrapper
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: false
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
  ]