@repobuddy/storybook 2.20.0 → 2.21.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
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { UserConfig } from "htmlfy";
3
+ import * as react from "react";
3
4
  import { ReactNode } from "react";
4
5
  import * as react_jsx_runtime0 from "react/jsx-runtime";
5
6
  import { AnyFunction, CreateTuple, IsStringLiteral, Properties, Tail } from "type-plus";
@@ -94,13 +95,7 @@ type StoryCardThemeState = Pick<StoryCardProps, 'status' | 'appearance'> & {
94
95
  * @param props - StoryCard component props
95
96
  * @returns A section element containing the card content
96
97
  */
97
- declare function StoryCard({
98
- status,
99
- appearance,
100
- className,
101
- children,
102
- title
103
- }: StoryCardProps): react_jsx_runtime0.JSX.Element;
98
+ declare const StoryCard: react.NamedExoticComponent<StoryCardProps>;
104
99
  //#endregion
105
100
  //#region src/decorators/show_doc_source.d.ts
106
101
  /**
package/esm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { isRunningInTest } from "@repobuddy/test";
3
3
  import { prettify } from "htmlfy";
4
- import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { cva } from "class-variance-authority";
7
7
  import { twJoin, twMerge } from "tailwind-merge";
@@ -48,7 +48,7 @@ function storyCardTheme(state, className) {
48
48
  if (!className) return state.defaultClassName;
49
49
  return twMerge(typeof className === "function" ? className(state) : twJoin(state.defaultClassName, className));
50
50
  }
51
- const storyCardVariants = cva("rbsb:flex rbsb:flex-col rbsb:gap-1 rbsb:py-3 rbsb:px-4 rbsb:rounded rbsb:border rbsb:border-solid rbsb:text-black rbsb:dark:text-gray-100", {
51
+ const storyCardVariants = cva("rbsb:py-3 rbsb:px-4 rbsb:rounded rbsb:border rbsb:border-solid rbsb:text-black rbsb:dark:text-gray-100", {
52
52
  variants: { appearance: {
53
53
  error: "rbsb:bg-red-100 rbsb:dark:bg-red-900 rbsb:border-red-300 rbsb:dark:border-red-700",
54
54
  warn: "rbsb:bg-yellow-100 rbsb:dark:bg-yellow-900 rbsb:border-yellow-300 rbsb:dark:border-yellow-700",
@@ -64,7 +64,7 @@ const storyCardVariants = cva("rbsb:flex rbsb:flex-col rbsb:gap-1 rbsb:py-3 rbsb
64
64
  * @param props - StoryCard component props
65
65
  * @returns A section element containing the card content
66
66
  */
67
- function StoryCard({ status, appearance, className, children, title }) {
67
+ const StoryCard = memo(function StoryCard({ status, appearance, className, children, title }) {
68
68
  const resolvedAppearance = resolveAppearance(appearance, status);
69
69
  return /* @__PURE__ */ jsxs("section", {
70
70
  className: storyCardTheme({
@@ -77,7 +77,7 @@ function StoryCard({ status, appearance, className, children, title }) {
77
77
  children: title
78
78
  }), children]
79
79
  });
80
- }
80
+ });
81
81
 
82
82
  //#endregion
83
83
  //#region src/utils/generate_key.ts
@@ -102,15 +102,12 @@ const StoryCardRegistryContext = createContext(null);
102
102
  * Ensures a story-card collection scope: creates the root container when no context exists,
103
103
  * otherwise renders the collector so this card participates in the existing scope.
104
104
  */
105
- function StoryCardScope({ Story, ...props }) {
105
+ const StoryCardScope = memo(function StoryCardScope(props) {
106
106
  const context = useContext(StoryCardRegistryContext);
107
- const collector = /* @__PURE__ */ jsx(StoryCardCollector, {
108
- Story,
109
- ...props
110
- });
107
+ const collector = /* @__PURE__ */ jsx(StoryCardCollector, { ...props });
111
108
  if (context === null) return /* @__PURE__ */ jsx(StoryCardContainer, { children: collector });
112
109
  return collector;
113
- }
110
+ });
114
111
  function StoryCardContainer({ children }) {
115
112
  const [cards, setCards] = useState([]);
116
113
  const contextValue = useMemo(() => ({
@@ -124,6 +121,12 @@ function StoryCardContainer({ children }) {
124
121
  },
125
122
  remove(key) {
126
123
  setCards((cards) => cards.filter((card) => card.key !== key));
124
+ },
125
+ update(key, card) {
126
+ setCards((cards) => cards.map((c) => c.key === key ? {
127
+ ...card,
128
+ key
129
+ } : c));
127
130
  }
128
131
  }), []);
129
132
  return /* @__PURE__ */ jsx(StoryCardRegistryContext.Provider, {
@@ -137,26 +140,36 @@ function StoryCardContainer({ children }) {
137
140
  })
138
141
  });
139
142
  }
140
- function StoryCardCollector({ Story, title, status, appearance, className, content }) {
143
+ const StoryCardCollector = memo(function StoryCardCollector({ Story, title, status, appearance, className, content }) {
141
144
  const context = useContext(StoryCardRegistryContext);
142
145
  const cardIdRef = useRef(null);
146
+ const entry = useMemo(() => ({
147
+ title,
148
+ status,
149
+ appearance,
150
+ className,
151
+ content
152
+ }), [
153
+ title,
154
+ status,
155
+ appearance,
156
+ className,
157
+ content
158
+ ]);
143
159
  useLayoutEffect(() => {
144
- if (cardIdRef.current === null) cardIdRef.current = context.add({
145
- title,
146
- status,
147
- appearance,
148
- className,
149
- content
150
- });
160
+ cardIdRef.current = context.add(entry);
151
161
  return () => {
152
162
  if (cardIdRef.current !== null) {
153
163
  context.remove(cardIdRef.current);
154
164
  cardIdRef.current = null;
155
165
  }
156
166
  };
157
- }, []);
167
+ }, [context]);
168
+ useLayoutEffect(() => {
169
+ if (cardIdRef.current !== null) context.update(cardIdRef.current, entry);
170
+ }, [context, entry]);
158
171
  return /* @__PURE__ */ jsx(Story, {});
159
- }
172
+ }, (prev, next) => prev.Story === next.Story);
160
173
 
161
174
  //#endregion
162
175
  //#region src/decorators/show_doc_source.tsx
@@ -173,6 +186,7 @@ const channel = addons.getChannel();
173
186
  * @returns A decorator function that shows the source code of a story above or below the rendered story
174
187
  */
175
188
  function showDocSource(options) {
189
+ if (isRunningInTest()) return (Story) => /* @__PURE__ */ jsx(Story, {});
176
190
  return (Story, { parameters: { docs, darkMode } }) => {
177
191
  const storedItem = window.localStorage.getItem("sb-addon-themes-3");
178
192
  const current = typeof storedItem === "string" ? JSON.parse(storedItem).current : darkMode?.current;
@@ -185,28 +199,39 @@ function showDocSource(options) {
185
199
  const code = typeof options?.source === "function" ? options?.source(originalSource) : options?.source ?? originalSource;
186
200
  const language = code === docs?.source?.originalSource ? void 0 : docs?.source?.language;
187
201
  const isOriginalSource = code === docs?.source?.originalSource;
188
- const sourceContent = /* @__PURE__ */ jsx(SyntaxHighlighter, {
202
+ const sourceContent = useMemo(() => /* @__PURE__ */ jsx(SyntaxHighlighter, {
203
+ "data-testid": "source-content",
189
204
  language,
190
205
  children: code
191
- });
206
+ }), [code, language]);
192
207
  const showBefore = options?.placement === "before";
193
- const sourceCardClassName = (state) => {
208
+ const sourceCardClassName = useCallback((state) => {
194
209
  const modifiedState = {
195
210
  ...state,
196
211
  defaultClassName: twJoin(state.defaultClassName, isOriginalSource && "rbsb:bg-transparent rbsb:dark:bg-transparent")
197
212
  };
198
213
  const className = options?.className;
199
214
  return typeof className === "function" ? className(modifiedState) : twJoin(modifiedState.defaultClassName, className);
200
- };
215
+ }, [options?.className, isOriginalSource]);
201
216
  const theme = convert(docs?.theme ?? (isDark ? themes.dark : themes.light));
217
+ function DocSourceCard({ children }) {
218
+ return /* @__PURE__ */ jsx("div", { children });
219
+ }
220
+ const scopeContent = useMemo(() => /* @__PURE__ */ jsx(DocSourceCard, { children: sourceContent }), [sourceContent]);
221
+ const scopeProps = useMemo(() => ({
222
+ Story,
223
+ content: scopeContent,
224
+ className: sourceCardClassName,
225
+ appearance: "source"
226
+ }), [
227
+ Story,
228
+ scopeContent,
229
+ sourceCardClassName
230
+ ]);
231
+ if (isRunningInTest()) return /* @__PURE__ */ jsx(Story, {});
202
232
  if (showBefore) return /* @__PURE__ */ jsx(ThemeProvider, {
203
233
  theme,
204
- children: /* @__PURE__ */ jsx(StoryCardScope, {
205
- Story,
206
- content: sourceContent,
207
- className: sourceCardClassName,
208
- appearance: "source"
209
- })
234
+ children: /* @__PURE__ */ jsx(StoryCardScope, { ...scopeProps })
210
235
  });
211
236
  return /* @__PURE__ */ jsx(ThemeProvider, {
212
237
  theme,
@@ -219,7 +244,7 @@ function showDocSource(options) {
219
244
  children: [/* @__PURE__ */ jsx(Story, {}), /* @__PURE__ */ jsx(StoryCard, {
220
245
  className: sourceCardClassName,
221
246
  appearance: "source",
222
- children: sourceContent
247
+ children: /* @__PURE__ */ jsx(DocSourceCard, { children: sourceContent })
223
248
  })]
224
249
  })
225
250
  });
@@ -308,8 +333,8 @@ function showDocSource(options) {
308
333
  * - The `status` option is deprecated; use `appearance` instead for the same behavior and additional variants (`source`, `output`).
309
334
  */
310
335
  function withStoryCard({ title, status, appearance, content: contentProp, className, ...rest } = {}) {
336
+ if (isRunningInTest()) return (Story) => /* @__PURE__ */ jsx(Story, {});
311
337
  return (Story, { parameters, viewMode }) => {
312
- if (viewMode === "docs") return /* @__PURE__ */ jsx(Story, {});
313
338
  const storyCardParam = parameters.storyCard;
314
339
  const finalTitle = title ?? storyCardParam?.title;
315
340
  const finalAppearance = appearance ?? storyCardParam?.appearance ?? status ?? storyCardParam?.status ?? "info";
@@ -317,8 +342,7 @@ function withStoryCard({ title, status, appearance, content: contentProp, classN
317
342
  const finalContent = contentProp ?? storyCardParam?.content;
318
343
  const finalClassName = className ?? storyCardParam?.className;
319
344
  const content = finalContent ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component;
320
- if (!content && !finalTitle) return /* @__PURE__ */ jsx(Story, {});
321
- return /* @__PURE__ */ jsx(StoryCardScope, {
345
+ const scopeProps = useMemo(() => ({
322
346
  Story,
323
347
  content,
324
348
  title: finalTitle,
@@ -326,7 +350,18 @@ function withStoryCard({ title, status, appearance, content: contentProp, classN
326
350
  appearance: finalAppearance,
327
351
  className: finalClassName,
328
352
  ...rest
329
- });
353
+ }), [
354
+ Story,
355
+ content,
356
+ finalTitle,
357
+ finalStatus,
358
+ finalAppearance,
359
+ finalClassName,
360
+ rest
361
+ ]);
362
+ if (viewMode === "docs") return /* @__PURE__ */ jsx(Story, {});
363
+ if (!content && !finalTitle) return /* @__PURE__ */ jsx(Story, {});
364
+ return /* @__PURE__ */ jsx(StoryCardScope, { ...scopeProps });
330
365
  };
331
366
  }
332
367
 
@@ -104,7 +104,21 @@ const removeBadge = {
104
104
  },
105
105
  tooltip: version === "next" ? "Will be removed in the next major release" : `Will be removed in version ${version}`
106
106
  };
107
- }
107
+ },
108
+ display: { sidebar: [
109
+ {
110
+ type: "component",
111
+ skipInherited: false
112
+ },
113
+ {
114
+ type: "story",
115
+ skipInherited: false
116
+ },
117
+ {
118
+ type: "docs",
119
+ skipInherited: false
120
+ }
121
+ ] }
108
122
  };
109
123
  /** Badge (⚠️) for stories that need updating. */
110
124
  const outdatedBadge = {
@@ -205,7 +219,13 @@ const sourceBadge = {
205
219
  },
206
220
  tooltip: "Source Code"
207
221
  },
208
- display: { mdx: false }
222
+ display: {
223
+ mdx: false,
224
+ sidebar: {
225
+ type: "story",
226
+ skipInherited: false
227
+ }
228
+ }
209
229
  };
210
230
  /** Badge (📸) for stories with snapshot tests. Shown in toolbar only, not in sidebar. */
211
231
  const snapshotBadge = {
@@ -234,7 +254,10 @@ const unitBadge = {
234
254
  },
235
255
  tooltip: "Unit Test"
236
256
  },
237
- display: { sidebar: true }
257
+ display: { sidebar: {
258
+ type: "story",
259
+ skipInherited: false
260
+ } }
238
261
  };
239
262
  /** Badge (🔗) for stories with integration tests. Hidden in sidebar. */
240
263
  const integrationBadge = {
@@ -247,7 +270,10 @@ const integrationBadge = {
247
270
  },
248
271
  tooltip: "Integration Test"
249
272
  },
250
- display: { sidebar: false }
273
+ display: { sidebar: {
274
+ type: "story",
275
+ skipInherited: false
276
+ } }
251
277
  };
252
278
  /** Badge (⌨️) for stories that demonstrate or test keyboard interaction. */
253
279
  const keyboardBadge = {
@@ -283,7 +309,11 @@ const useCaseBadge = {
283
309
  borderColor: "transparent"
284
310
  },
285
311
  tooltip: "Use Case"
286
- }
312
+ },
313
+ display: { sidebar: {
314
+ type: "story",
315
+ skipInherited: false
316
+ } }
287
317
  };
288
318
  /** Badge (✨) for example or demo stories. */
289
319
  const exampleBadge = {
@@ -295,7 +325,11 @@ const exampleBadge = {
295
325
  borderColor: "transparent"
296
326
  },
297
327
  tooltip: "Example"
298
- }
328
+ },
329
+ display: { sidebar: {
330
+ type: "story",
331
+ skipInherited: false
332
+ } }
299
333
  };
300
334
  /**
301
335
  * Configuration for story tag badges that appear in the Storybook sidebar.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repobuddy/storybook",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "Storybook repo buddy",
5
5
  "keywords": [
6
6
  "storybook",
@@ -92,6 +92,7 @@
92
92
  "storybook": "^10.2.8",
93
93
  "storybook-addon-code-editor": "^6.1.3",
94
94
  "storybook-addon-tag-badges": "^3.0.6",
95
+ "storybook-addon-vis": "^3.1.2",
95
96
  "tailwindcss": "^4.1.17",
96
97
  "tsdown": "^0.20.0",
97
98
  "vite": "^7.3.0",
@@ -1,5 +1,5 @@
1
1
  import { cva } from 'class-variance-authority'
2
- import type { ReactNode } from 'react'
2
+ import { memo, type ReactNode } from 'react'
3
3
  import { twJoin, twMerge } from 'tailwind-merge'
4
4
 
5
5
  /**
@@ -67,7 +67,7 @@ function storyCardTheme(state: StoryCardThemeState, className: StoryCardProps['c
67
67
  }
68
68
 
69
69
  const storyCardVariants = cva(
70
- 'rbsb:flex rbsb:flex-col rbsb:gap-1 rbsb:py-3 rbsb:px-4 rbsb:rounded rbsb:border rbsb:border-solid rbsb:text-black rbsb:dark:text-gray-100',
70
+ 'rbsb:py-3 rbsb:px-4 rbsb:rounded rbsb:border rbsb:border-solid rbsb:text-black rbsb:dark:text-gray-100',
71
71
  {
72
72
  variants: {
73
73
  appearance: {
@@ -90,7 +90,7 @@ const storyCardVariants = cva(
90
90
  * @param props - StoryCard component props
91
91
  * @returns A section element containing the card content
92
92
  */
93
- export function StoryCard({ status, appearance, className, children, title }: StoryCardProps) {
93
+ export const StoryCard = memo(function StoryCard({ status, appearance, className, children, title }: StoryCardProps) {
94
94
  const resolvedAppearance = resolveAppearance(appearance, status)
95
95
  const state: StoryCardThemeState = {
96
96
  status,
@@ -103,4 +103,4 @@ export function StoryCard({ status, appearance, className, children, title }: St
103
103
  {children}
104
104
  </section>
105
105
  )
106
- }
106
+ })
@@ -11,6 +11,7 @@ export type StoryCardEntry = Omit<StoryCardProps, 'children'> & { content?: Reac
11
11
  export interface StoryCardRegistryContextValue {
12
12
  add: (card: StoryCardEntry) => string
13
13
  remove: (id: string) => void
14
+ update: (id: string, card: StoryCardEntry) => void
14
15
  }
15
16
 
16
17
  export const StoryCardRegistryContext = createContext<StoryCardRegistryContextValue | null>(null)
@@ -1,4 +1,4 @@
1
- import { type ComponentType, type ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'
1
+ import { type ComponentType, memo, type ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'
2
2
  import { StoryCard } from '../components/story_card.js'
3
3
  import { generateKey } from '../utils/generate_key.js'
4
4
  import {
@@ -13,16 +13,16 @@ export type StoryCardScopeProps = { Story: ComponentType } & StoryCardEntry
13
13
  * Ensures a story-card collection scope: creates the root container when no context exists,
14
14
  * otherwise renders the collector so this card participates in the existing scope.
15
15
  */
16
- export function StoryCardScope({ Story, ...props }: StoryCardScopeProps) {
16
+ export const StoryCardScope = memo(function StoryCardScope(props: StoryCardScopeProps) {
17
17
  const context = useContext(StoryCardRegistryContext)
18
- const collector = <StoryCardCollector Story={Story} {...props} />
18
+ const collector = <StoryCardCollector {...props} />
19
19
 
20
20
  if (context === null) {
21
21
  return <StoryCardContainer>{collector}</StoryCardContainer>
22
22
  }
23
23
 
24
24
  return collector
25
- }
25
+ })
26
26
 
27
27
  function StoryCardContainer({ children }: { children: ReactNode }) {
28
28
  const [cards, setCards] = useState<StoryCardEntryWithKey[]>([])
@@ -36,6 +36,9 @@ function StoryCardContainer({ children }: { children: ReactNode }) {
36
36
  },
37
37
  remove(key) {
38
38
  setCards((cards) => cards.filter((card) => card.key !== key))
39
+ },
40
+ update(key, card) {
41
+ setCards((cards) => cards.map((c) => (c.key === key ? { ...card, key } : c)))
39
42
  }
40
43
  }),
41
44
  []
@@ -59,25 +62,35 @@ type StoryCardEntryWithKey = StoryCardEntry & { key: string }
59
62
 
60
63
  interface StoryCardCollectorProps extends StoryCardScopeProps {}
61
64
 
62
- function StoryCardCollector({ Story, title, status, appearance, 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)
65
+ const StoryCardCollector = memo(
66
+ function StoryCardCollector({ Story, title, status, appearance, className, content }: StoryCardCollectorProps) {
67
+ const context = useContext(StoryCardRegistryContext)!
68
+ const cardIdRef = useRef<string | null>(null)
66
69
 
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, appearance, className, content })
72
- }
70
+ const entry = useMemo(
71
+ () => ({ title, status, appearance, className, content }),
72
+ [title, status, appearance, className, content]
73
+ )
73
74
 
74
- return () => {
75
+ // Register on mount, unregister on unmount only
76
+ useLayoutEffect(() => {
77
+ cardIdRef.current = context.add(entry)
78
+ return () => {
79
+ if (cardIdRef.current !== null) {
80
+ context.remove(cardIdRef.current)
81
+ cardIdRef.current = null
82
+ }
83
+ }
84
+ }, [context])
85
+
86
+ // Update registry when entry changes (avoids remove+add churn)
87
+ useLayoutEffect(() => {
75
88
  if (cardIdRef.current !== null) {
76
- context.remove(cardIdRef.current)
77
- cardIdRef.current = null
89
+ context.update(cardIdRef.current, entry)
78
90
  }
79
- }
80
- }, [])
91
+ }, [context, entry])
81
92
 
82
- return <Story />
83
- }
93
+ return <Story />
94
+ },
95
+ (prev, next) => prev.Story === next.Story
96
+ )
@@ -1,4 +1,5 @@
1
- import { useEffect, useState } from 'react'
1
+ import { isRunningInTest } from '@repobuddy/test'
2
+ import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
2
3
  import { SyntaxHighlighter } from 'storybook/internal/components'
3
4
  import type { Args, DecoratorFunction, Renderer } from 'storybook/internal/csf'
4
5
  import { addons } from 'storybook/preview-api'
@@ -32,6 +33,10 @@ export function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Arg
32
33
  placement?: 'before' | 'after' | undefined
33
34
  }
34
35
  ): DecoratorFunction<TRenderer, TArgs> {
36
+ if (isRunningInTest()) {
37
+ return (Story) => <Story />
38
+ }
39
+
35
40
  return (Story, { parameters: { docs, darkMode } }) => {
36
41
  // This is a workaround to get the current dark mode from `@storybook-community/storybook-dark-mode` without referencing it.
37
42
  const storedItem = window.localStorage.getItem('sb-addon-themes-3')
@@ -55,40 +60,68 @@ export function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Arg
55
60
 
56
61
  const isOriginalSource = code === docs?.source?.originalSource
57
62
 
58
- const sourceContent = <SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
63
+ const sourceContent = useMemo(
64
+ () => (
65
+ <SyntaxHighlighter data-testid="source-content" language={language}>
66
+ {code}
67
+ </SyntaxHighlighter>
68
+ ),
69
+ [code, language]
70
+ )
59
71
 
60
72
  const showBefore = options?.placement === 'before'
61
73
 
62
- const sourceCardClassName = (
63
- state: Pick<StoryCardProps, 'status' | 'appearance'> & { defaultClassName: string }
64
- ) => {
65
- const modifiedState = {
66
- ...state,
67
- defaultClassName: twJoin(
68
- state.defaultClassName,
69
- isOriginalSource && 'rbsb:bg-transparent rbsb:dark:bg-transparent'
70
- )
71
- }
72
-
73
- const className = options?.className
74
- return typeof className === 'function'
75
- ? className(modifiedState)
76
- : twJoin(modifiedState.defaultClassName, className)
77
- }
74
+ const sourceCardClassName = useCallback(
75
+ (state: Pick<StoryCardProps, 'status' | 'appearance'> & { defaultClassName: string }) => {
76
+ const modifiedState = {
77
+ ...state,
78
+ defaultClassName: twJoin(
79
+ state.defaultClassName,
80
+ isOriginalSource && 'rbsb:bg-transparent rbsb:dark:bg-transparent'
81
+ )
82
+ }
83
+
84
+ const className = options?.className
85
+ return typeof className === 'function'
86
+ ? className(modifiedState)
87
+ : twJoin(modifiedState.defaultClassName, className)
88
+ },
89
+ [options?.className, isOriginalSource]
90
+ )
78
91
 
79
92
  const theme = convert(docs?.theme ?? (isDark ? themes.dark : themes.light))
80
93
 
94
+ function DocSourceCard({ children }: { children: ReactNode }) {
95
+ return <div>{children}</div>
96
+ }
97
+
98
+ const scopeContent = useMemo(() => <DocSourceCard>{sourceContent}</DocSourceCard>, [sourceContent])
99
+
100
+ const scopeProps = useMemo(
101
+ () => ({
102
+ Story,
103
+ content: scopeContent,
104
+ className: sourceCardClassName,
105
+ appearance: 'source' as const
106
+ }),
107
+ [Story, scopeContent, sourceCardClassName]
108
+ )
109
+
110
+ if (isRunningInTest()) {
111
+ return <Story />
112
+ }
113
+
81
114
  if (showBefore) {
82
115
  return (
83
116
  <ThemeProvider theme={theme}>
84
- <StoryCardScope Story={Story} content={sourceContent} className={sourceCardClassName} appearance="source" />
117
+ <StoryCardScope {...scopeProps} />
85
118
  </ThemeProvider>
86
119
  )
87
120
  }
88
121
 
89
122
  const storyCard = (
90
123
  <StoryCard className={sourceCardClassName} appearance="source">
91
- {sourceContent}
124
+ <DocSourceCard>{sourceContent}</DocSourceCard>
92
125
  </StoryCard>
93
126
  )
94
127
 
@@ -1,4 +1,5 @@
1
- import type { ReactNode } from 'react'
1
+ import { isRunningInTest } from '@repobuddy/test'
2
+ import { type ReactNode, useMemo } from 'react'
2
3
  import type { DecoratorFunction, Renderer } from 'storybook/internal/csf'
3
4
  import type { StoryCardProps, StoryCardStatus } from '../components/story_card.js'
4
5
  import { StoryCardScope } from '../contexts/_story_card_scope.js'
@@ -119,9 +120,10 @@ export function withStoryCard<TRenderer extends Renderer = Renderer>({
119
120
  className,
120
121
  ...rest
121
122
  }: WithStoryCardProps = {}): DecoratorFunction<TRenderer> {
123
+ if (isRunningInTest()) {
124
+ return (Story) => <Story />
125
+ }
122
126
  return (Story, { parameters, viewMode }) => {
123
- if (viewMode === 'docs') return <Story />
124
-
125
127
  // Get story card config from parameters if available
126
128
  const storyCardParam = (parameters as Partial<StoryCardParam>).storyCard
127
129
  // Decorator props override parameter values
@@ -134,18 +136,23 @@ export function withStoryCard<TRenderer extends Renderer = Renderer>({
134
136
 
135
137
  // Fallback to docs description if no content provided
136
138
  const content = finalContent ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component
137
- if (!content && !finalTitle) return <Story />
138
139
 
139
- return (
140
- <StoryCardScope
141
- Story={Story}
142
- content={content}
143
- title={finalTitle}
144
- status={finalStatus}
145
- appearance={finalAppearance}
146
- className={finalClassName}
147
- {...rest}
148
- />
140
+ const scopeProps = useMemo(
141
+ () => ({
142
+ Story,
143
+ content,
144
+ title: finalTitle,
145
+ status: finalStatus,
146
+ appearance: finalAppearance,
147
+ className: finalClassName,
148
+ ...rest
149
+ }),
150
+ [Story, content, finalTitle, finalStatus, finalAppearance, finalClassName, rest]
149
151
  )
152
+
153
+ if (viewMode === 'docs') return <Story />
154
+ if (!content && !finalTitle) return <Story />
155
+
156
+ return <StoryCardScope {...scopeProps} />
150
157
  }
151
158
  }
@@ -152,6 +152,22 @@ export const removeBadge: TagBadgeParameter = {
152
152
  tooltip:
153
153
  version === 'next' ? 'Will be removed in the next major release' : `Will be removed in version ${version}`
154
154
  }
155
+ },
156
+ display: {
157
+ sidebar: [
158
+ {
159
+ type: 'component',
160
+ skipInherited: false
161
+ },
162
+ {
163
+ type: 'story',
164
+ skipInherited: false
165
+ },
166
+ {
167
+ type: 'docs',
168
+ skipInherited: false
169
+ }
170
+ ]
155
171
  }
156
172
  }
157
173
 
@@ -270,7 +286,11 @@ export const sourceBadge: TagBadgeParameter = {
270
286
  tooltip: 'Source Code'
271
287
  },
272
288
  display: {
273
- mdx: false
289
+ mdx: false,
290
+ sidebar: {
291
+ type: 'story',
292
+ skipInherited: false
293
+ }
274
294
  }
275
295
  }
276
296
 
@@ -303,7 +323,10 @@ export const unitBadge: TagBadgeParameter = {
303
323
  tooltip: 'Unit Test'
304
324
  },
305
325
  display: {
306
- sidebar: true
326
+ sidebar: {
327
+ type: 'story',
328
+ skipInherited: false
329
+ }
307
330
  }
308
331
  }
309
332
 
@@ -319,7 +342,10 @@ export const integrationBadge: TagBadgeParameter = {
319
342
  tooltip: 'Integration Test'
320
343
  },
321
344
  display: {
322
- sidebar: false
345
+ sidebar: {
346
+ type: 'story',
347
+ skipInherited: false
348
+ }
323
349
  }
324
350
  }
325
351
 
@@ -359,6 +385,12 @@ export const useCaseBadge: TagBadgeParameter = {
359
385
  borderColor: 'transparent'
360
386
  },
361
387
  tooltip: 'Use Case'
388
+ },
389
+ display: {
390
+ sidebar: {
391
+ type: 'story',
392
+ skipInherited: false
393
+ }
362
394
  }
363
395
  }
364
396
 
@@ -372,6 +404,12 @@ export const exampleBadge: TagBadgeParameter = {
372
404
  borderColor: 'transparent'
373
405
  },
374
406
  tooltip: 'Example'
407
+ },
408
+ display: {
409
+ sidebar: {
410
+ type: 'story',
411
+ skipInherited: false
412
+ }
375
413
  }
376
414
  }
377
415
 
package/styles.css CHANGED
@@ -68,9 +68,6 @@
68
68
  .rbsb\:flex-col {
69
69
  flex-direction: column;
70
70
  }
71
- .rbsb\:gap-1 {
72
- gap: calc(var(--rbsb-spacing) * 1);
73
- }
74
71
  .rbsb\:gap-2 {
75
72
  gap: calc(var(--rbsb-spacing) * 2);
76
73
  }