@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 +2 -7
- package/esm/index.js +70 -35
- package/esm/storybook-addon-tag-badges/index.js +40 -6
- package/package.json +2 -1
- package/src/components/story_card.tsx +4 -4
- package/src/contexts/_story_card_registry_context.tsx +1 -0
- package/src/contexts/_story_card_scope.tsx +34 -21
- package/src/decorators/show_doc_source.tsx +53 -20
- package/src/decorators/with_story_card.tsx +21 -14
- package/src/storybook-addon-tag-badges/tag_badges.ts +41 -3
- package/styles.css +0 -3
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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:
|
|
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(
|
|
16
|
+
export const StoryCardScope = memo(function StoryCardScope(props: StoryCardScopeProps) {
|
|
17
17
|
const context = useContext(StoryCardRegistryContext)
|
|
18
|
-
const collector = <StoryCardCollector
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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.
|
|
77
|
-
cardIdRef.current = null
|
|
89
|
+
context.update(cardIdRef.current, entry)
|
|
78
90
|
}
|
|
79
|
-
}
|
|
80
|
-
}, [])
|
|
91
|
+
}, [context, entry])
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
}
|
|
93
|
+
return <Story />
|
|
94
|
+
},
|
|
95
|
+
(prev, next) => prev.Story === next.Story
|
|
96
|
+
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
Story
|
|
142
|
-
content
|
|
143
|
-
title
|
|
144
|
-
status
|
|
145
|
-
appearance
|
|
146
|
-
className
|
|
147
|
-
|
|
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:
|
|
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:
|
|
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
|
|