@repobuddy/storybook 2.20.1 → 2.21.1
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 +3 -8
- package/esm/index.js +73 -36
- package/package.json +3 -2
- 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 +62 -20
- package/src/decorators/show_doc_source.tsx +53 -20
- package/src/decorators/with_story_card.tsx +5 -0
- package/src/index.ts +1 -1
- package/styles.css +1 -4
- /package/src/arg-types/{fn-to-arg-types.ts → fn-to-arg.types.ts} +0 -0
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";
|
|
@@ -8,7 +9,7 @@ import { Args, DecoratorFunction, Renderer } from "storybook/internal/csf";
|
|
|
8
9
|
import { Decorator, Meta, StoryContext, StoryObj, StrictArgs } from "@storybook/react-vite";
|
|
9
10
|
export * from "@repobuddy/test";
|
|
10
11
|
|
|
11
|
-
//#region src/arg-types/fn-to-arg
|
|
12
|
+
//#region src/arg-types/fn-to-arg.types.d.ts
|
|
12
13
|
/**
|
|
13
14
|
* Converts a function's parameter types to `Args` type for Storybook.
|
|
14
15
|
* Each name maps to the parameter type at the same index in F.
|
|
@@ -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,8 +1,8 @@
|
|
|
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";
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { cva } from "class-variance-authority";
|
|
7
7
|
import { twJoin, twMerge } from "tailwind-merge";
|
|
8
8
|
import { SyntaxHighlighter } from "storybook/internal/components";
|
|
@@ -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,23 @@ 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
|
+
});
|
|
111
|
+
/** Renders cards from registry state without re-rendering when only children change (avoids cascade). */
|
|
112
|
+
const StoryCardList = memo(function StoryCardList({ cards }) {
|
|
113
|
+
return /* @__PURE__ */ jsx(Fragment, { children: cards.map(({ content, key, ...rest }) => /* @__PURE__ */ jsx(StoryCard, {
|
|
114
|
+
...rest,
|
|
115
|
+
children: content
|
|
116
|
+
}, key)) });
|
|
117
|
+
});
|
|
118
|
+
/** Keeps container children from re-rendering when container state (cards) updates. */
|
|
119
|
+
const StableScopeChildren = memo(function StableScopeChildren({ children }) {
|
|
120
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
121
|
+
});
|
|
114
122
|
function StoryCardContainer({ children }) {
|
|
115
123
|
const [cards, setCards] = useState([]);
|
|
116
124
|
const contextValue = useMemo(() => ({
|
|
@@ -124,39 +132,55 @@ function StoryCardContainer({ children }) {
|
|
|
124
132
|
},
|
|
125
133
|
remove(key) {
|
|
126
134
|
setCards((cards) => cards.filter((card) => card.key !== key));
|
|
135
|
+
},
|
|
136
|
+
update(key, card) {
|
|
137
|
+
setCards((cards) => cards.map((c) => c.key === key ? {
|
|
138
|
+
...card,
|
|
139
|
+
key
|
|
140
|
+
} : c));
|
|
127
141
|
}
|
|
128
142
|
}), []);
|
|
129
143
|
return /* @__PURE__ */ jsx(StoryCardRegistryContext.Provider, {
|
|
130
144
|
value: contextValue,
|
|
131
145
|
children: /* @__PURE__ */ jsxs("div", {
|
|
132
146
|
className: "rbsb:flex rbsb:flex-col rbsb:gap-2",
|
|
133
|
-
children: [
|
|
134
|
-
...rest,
|
|
135
|
-
children: content
|
|
136
|
-
}, key)), children]
|
|
147
|
+
children: [/* @__PURE__ */ jsx(StoryCardList, { cards }), /* @__PURE__ */ jsx(StableScopeChildren, { children })]
|
|
137
148
|
})
|
|
138
149
|
});
|
|
139
150
|
}
|
|
140
|
-
function
|
|
151
|
+
function entryPropsEqual(a, b) {
|
|
152
|
+
return a.Story === b.Story && a.title === b.title && a.status === b.status && a.appearance === b.appearance && a.className === b.className && a.content === b.content;
|
|
153
|
+
}
|
|
154
|
+
const StoryCardCollector = memo(function StoryCardCollector({ Story, title, status, appearance, className, content }) {
|
|
141
155
|
const context = useContext(StoryCardRegistryContext);
|
|
142
156
|
const cardIdRef = useRef(null);
|
|
157
|
+
const entry = useMemo(() => ({
|
|
158
|
+
title,
|
|
159
|
+
status,
|
|
160
|
+
appearance,
|
|
161
|
+
className,
|
|
162
|
+
content
|
|
163
|
+
}), [
|
|
164
|
+
title,
|
|
165
|
+
status,
|
|
166
|
+
appearance,
|
|
167
|
+
className,
|
|
168
|
+
content
|
|
169
|
+
]);
|
|
143
170
|
useLayoutEffect(() => {
|
|
144
|
-
|
|
145
|
-
title,
|
|
146
|
-
status,
|
|
147
|
-
appearance,
|
|
148
|
-
className,
|
|
149
|
-
content
|
|
150
|
-
});
|
|
171
|
+
cardIdRef.current = context.add(entry);
|
|
151
172
|
return () => {
|
|
152
173
|
if (cardIdRef.current !== null) {
|
|
153
174
|
context.remove(cardIdRef.current);
|
|
154
175
|
cardIdRef.current = null;
|
|
155
176
|
}
|
|
156
177
|
};
|
|
157
|
-
}, []);
|
|
178
|
+
}, [context]);
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
if (cardIdRef.current !== null) context.update(cardIdRef.current, entry);
|
|
181
|
+
}, [context, entry]);
|
|
158
182
|
return /* @__PURE__ */ jsx(Story, {});
|
|
159
|
-
}
|
|
183
|
+
}, entryPropsEqual);
|
|
160
184
|
|
|
161
185
|
//#endregion
|
|
162
186
|
//#region src/decorators/show_doc_source.tsx
|
|
@@ -173,6 +197,7 @@ const channel = addons.getChannel();
|
|
|
173
197
|
* @returns A decorator function that shows the source code of a story above or below the rendered story
|
|
174
198
|
*/
|
|
175
199
|
function showDocSource(options) {
|
|
200
|
+
if (isRunningInTest()) return (Story) => /* @__PURE__ */ jsx(Story, {});
|
|
176
201
|
return (Story, { parameters: { docs, darkMode } }) => {
|
|
177
202
|
const storedItem = window.localStorage.getItem("sb-addon-themes-3");
|
|
178
203
|
const current = typeof storedItem === "string" ? JSON.parse(storedItem).current : darkMode?.current;
|
|
@@ -185,28 +210,39 @@ function showDocSource(options) {
|
|
|
185
210
|
const code = typeof options?.source === "function" ? options?.source(originalSource) : options?.source ?? originalSource;
|
|
186
211
|
const language = code === docs?.source?.originalSource ? void 0 : docs?.source?.language;
|
|
187
212
|
const isOriginalSource = code === docs?.source?.originalSource;
|
|
188
|
-
const sourceContent = /* @__PURE__ */ jsx(SyntaxHighlighter, {
|
|
213
|
+
const sourceContent = useMemo(() => /* @__PURE__ */ jsx(SyntaxHighlighter, {
|
|
214
|
+
"data-testid": "source-content",
|
|
189
215
|
language,
|
|
190
216
|
children: code
|
|
191
|
-
});
|
|
217
|
+
}), [code, language]);
|
|
192
218
|
const showBefore = options?.placement === "before";
|
|
193
|
-
const sourceCardClassName = (state) => {
|
|
219
|
+
const sourceCardClassName = useCallback((state) => {
|
|
194
220
|
const modifiedState = {
|
|
195
221
|
...state,
|
|
196
222
|
defaultClassName: twJoin(state.defaultClassName, isOriginalSource && "rbsb:bg-transparent rbsb:dark:bg-transparent")
|
|
197
223
|
};
|
|
198
224
|
const className = options?.className;
|
|
199
225
|
return typeof className === "function" ? className(modifiedState) : twJoin(modifiedState.defaultClassName, className);
|
|
200
|
-
};
|
|
226
|
+
}, [options?.className, isOriginalSource]);
|
|
201
227
|
const theme = convert(docs?.theme ?? (isDark ? themes.dark : themes.light));
|
|
228
|
+
function DocSourceCard({ children }) {
|
|
229
|
+
return /* @__PURE__ */ jsx("div", { children });
|
|
230
|
+
}
|
|
231
|
+
const scopeContent = useMemo(() => /* @__PURE__ */ jsx(DocSourceCard, { children: sourceContent }), [sourceContent]);
|
|
232
|
+
const scopeProps = useMemo(() => ({
|
|
233
|
+
Story,
|
|
234
|
+
content: scopeContent,
|
|
235
|
+
className: sourceCardClassName,
|
|
236
|
+
appearance: "source"
|
|
237
|
+
}), [
|
|
238
|
+
Story,
|
|
239
|
+
scopeContent,
|
|
240
|
+
sourceCardClassName
|
|
241
|
+
]);
|
|
242
|
+
if (isRunningInTest()) return /* @__PURE__ */ jsx(Story, {});
|
|
202
243
|
if (showBefore) return /* @__PURE__ */ jsx(ThemeProvider, {
|
|
203
244
|
theme,
|
|
204
|
-
children: /* @__PURE__ */ jsx(StoryCardScope, {
|
|
205
|
-
Story,
|
|
206
|
-
content: sourceContent,
|
|
207
|
-
className: sourceCardClassName,
|
|
208
|
-
appearance: "source"
|
|
209
|
-
})
|
|
245
|
+
children: /* @__PURE__ */ jsx(StoryCardScope, { ...scopeProps })
|
|
210
246
|
});
|
|
211
247
|
return /* @__PURE__ */ jsx(ThemeProvider, {
|
|
212
248
|
theme,
|
|
@@ -219,7 +255,7 @@ function showDocSource(options) {
|
|
|
219
255
|
children: [/* @__PURE__ */ jsx(Story, {}), /* @__PURE__ */ jsx(StoryCard, {
|
|
220
256
|
className: sourceCardClassName,
|
|
221
257
|
appearance: "source",
|
|
222
|
-
children: sourceContent
|
|
258
|
+
children: /* @__PURE__ */ jsx(DocSourceCard, { children: sourceContent })
|
|
223
259
|
})]
|
|
224
260
|
})
|
|
225
261
|
});
|
|
@@ -308,6 +344,7 @@ function showDocSource(options) {
|
|
|
308
344
|
* - The `status` option is deprecated; use `appearance` instead for the same behavior and additional variants (`source`, `output`).
|
|
309
345
|
*/
|
|
310
346
|
function withStoryCard({ title, status, appearance, content: contentProp, className, ...rest } = {}) {
|
|
347
|
+
if (isRunningInTest()) return (Story) => /* @__PURE__ */ jsx(Story, {});
|
|
311
348
|
return (Story, { parameters, viewMode }) => {
|
|
312
349
|
if (viewMode === "docs") return /* @__PURE__ */ jsx(Story, {});
|
|
313
350
|
const storyCardParam = parameters.storyCard;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repobuddy/storybook",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.1",
|
|
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",
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
"build:code": "tsdown",
|
|
120
121
|
"build:css": "tailwindcss -i ./tailwind.css -o ./styles.css",
|
|
121
122
|
"clean": "rimraf .turbo coverage cjs esm storybook-static *.tsbuildinfo",
|
|
122
|
-
"
|
|
123
|
+
"cov": "vitest run --coverage",
|
|
123
124
|
"sb": "storybook dev -p 6006",
|
|
124
125
|
"sb:build": "storybook build",
|
|
125
126
|
"test": "vitest run",
|
|
@@ -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,34 @@ 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
|
+
|
|
27
|
+
/** Renders cards from registry state without re-rendering when only children change (avoids cascade). */
|
|
28
|
+
const StoryCardList = memo(function StoryCardList({ cards }: { cards: StoryCardEntryWithKey[] }) {
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{cards.map(({ content, key, ...rest }) => (
|
|
32
|
+
<StoryCard key={key} {...rest}>
|
|
33
|
+
{content}
|
|
34
|
+
</StoryCard>
|
|
35
|
+
))}
|
|
36
|
+
</>
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/** Keeps container children from re-rendering when container state (cards) updates. */
|
|
41
|
+
const StableScopeChildren = memo(function StableScopeChildren({ children }: { children: ReactNode }) {
|
|
42
|
+
return <>{children}</>
|
|
43
|
+
})
|
|
26
44
|
|
|
27
45
|
function StoryCardContainer({ children }: { children: ReactNode }) {
|
|
28
46
|
const [cards, setCards] = useState<StoryCardEntryWithKey[]>([])
|
|
@@ -36,6 +54,9 @@ function StoryCardContainer({ children }: { children: ReactNode }) {
|
|
|
36
54
|
},
|
|
37
55
|
remove(key) {
|
|
38
56
|
setCards((cards) => cards.filter((card) => card.key !== key))
|
|
57
|
+
},
|
|
58
|
+
update(key, card) {
|
|
59
|
+
setCards((cards) => cards.map((c) => (c.key === key ? { ...card, key } : c)))
|
|
39
60
|
}
|
|
40
61
|
}),
|
|
41
62
|
[]
|
|
@@ -44,12 +65,8 @@ function StoryCardContainer({ children }: { children: ReactNode }) {
|
|
|
44
65
|
return (
|
|
45
66
|
<StoryCardRegistryContext.Provider value={contextValue}>
|
|
46
67
|
<div className="rbsb:flex rbsb:flex-col rbsb:gap-2">
|
|
47
|
-
{cards
|
|
48
|
-
|
|
49
|
-
{content}
|
|
50
|
-
</StoryCard>
|
|
51
|
-
))}
|
|
52
|
-
{children}
|
|
68
|
+
<StoryCardList cards={cards} />
|
|
69
|
+
<StableScopeChildren>{children}</StableScopeChildren>
|
|
53
70
|
</div>
|
|
54
71
|
</StoryCardRegistryContext.Provider>
|
|
55
72
|
)
|
|
@@ -59,25 +76,50 @@ type StoryCardEntryWithKey = StoryCardEntry & { key: string }
|
|
|
59
76
|
|
|
60
77
|
interface StoryCardCollectorProps extends StoryCardScopeProps {}
|
|
61
78
|
|
|
62
|
-
function
|
|
63
|
-
|
|
79
|
+
function entryPropsEqual(a: StoryCardCollectorProps, b: StoryCardCollectorProps): boolean {
|
|
80
|
+
return (
|
|
81
|
+
a.Story === b.Story &&
|
|
82
|
+
a.title === b.title &&
|
|
83
|
+
a.status === b.status &&
|
|
84
|
+
a.appearance === b.appearance &&
|
|
85
|
+
a.className === b.className &&
|
|
86
|
+
a.content === b.content
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const StoryCardCollector = memo(function StoryCardCollector({
|
|
91
|
+
Story,
|
|
92
|
+
title,
|
|
93
|
+
status,
|
|
94
|
+
appearance,
|
|
95
|
+
className,
|
|
96
|
+
content
|
|
97
|
+
}: StoryCardCollectorProps) {
|
|
64
98
|
const context = useContext(StoryCardRegistryContext)!
|
|
65
99
|
const cardIdRef = useRef<string | null>(null)
|
|
66
100
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
cardIdRef.current = context.add({ title, status, appearance, className, content })
|
|
72
|
-
}
|
|
101
|
+
const entry = useMemo(
|
|
102
|
+
() => ({ title, status, appearance, className, content }),
|
|
103
|
+
[title, status, appearance, className, content]
|
|
104
|
+
)
|
|
73
105
|
|
|
106
|
+
// Register on mount, unregister on unmount only
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
cardIdRef.current = context.add(entry)
|
|
74
109
|
return () => {
|
|
75
110
|
if (cardIdRef.current !== null) {
|
|
76
111
|
context.remove(cardIdRef.current)
|
|
77
112
|
cardIdRef.current = null
|
|
78
113
|
}
|
|
79
114
|
}
|
|
80
|
-
}, [])
|
|
115
|
+
}, [context])
|
|
116
|
+
|
|
117
|
+
// Update registry when entry changes (avoids remove+add churn)
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
if (cardIdRef.current !== null) {
|
|
120
|
+
context.update(cardIdRef.current, entry)
|
|
121
|
+
}
|
|
122
|
+
}, [context, entry])
|
|
81
123
|
|
|
82
124
|
return <Story />
|
|
83
|
-
}
|
|
125
|
+
}, entryPropsEqual)
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { isRunningInTest } from '@repobuddy/test'
|
|
1
2
|
import type { ReactNode } from 'react'
|
|
2
3
|
import type { DecoratorFunction, Renderer } from 'storybook/internal/csf'
|
|
3
4
|
import type { StoryCardProps, StoryCardStatus } from '../components/story_card.js'
|
|
@@ -119,6 +120,9 @@ 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
127
|
if (viewMode === 'docs') return <Story />
|
|
124
128
|
|
|
@@ -134,6 +138,7 @@ export function withStoryCard<TRenderer extends Renderer = Renderer>({
|
|
|
134
138
|
|
|
135
139
|
// Fallback to docs description if no content provided
|
|
136
140
|
const content = finalContent ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component
|
|
141
|
+
|
|
137
142
|
if (!content && !finalTitle) return <Story />
|
|
138
143
|
|
|
139
144
|
return (
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from '@repobuddy/test'
|
|
2
|
-
export type * from './arg-types/fn-to-arg
|
|
2
|
+
export type * from './arg-types/fn-to-arg.types.ts'
|
|
3
3
|
export * from './components/show_html.tsx'
|
|
4
4
|
export * from './components/story_card.tsx'
|
|
5
5
|
export * from './decorators/show_doc_source.tsx'
|
package/styles.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! tailwindcss v4.
|
|
1
|
+
/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */
|
|
2
2
|
@layer properties;
|
|
3
3
|
@layer theme {
|
|
4
4
|
:root, :host {
|
|
@@ -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
|
}
|
|
File without changes
|