@repobuddy/storybook 2.1.2 → 2.2.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,7 +1,10 @@
1
+
1
2
  import { UserConfig } from "htmlfy";
3
+ import { ReactNode } from "react";
2
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
5
  import { ClassNameProps, StyleProps } from "@just-web/css";
4
6
  import { Args, DecoratorFunction, Renderer } from "storybook/internal/csf";
7
+ import { ClassValue } from "class-variance-authority/types";
5
8
  import { Decorator, Meta, StoryContext, StoryObj, StrictArgs } from "@storybook/react-vite";
6
9
  export * from "@repobuddy/test";
7
10
 
@@ -32,6 +35,112 @@ declare function ShowHtml({
32
35
  */
33
36
  declare function showDocSource<TRenderer extends Renderer = Renderer, TArgs = Args>(): DecoratorFunction<TRenderer, TArgs>;
34
37
  //#endregion
38
+ //#region src/decorators/with_story_card.d.ts
39
+ type StoryCardProps = {
40
+ /**
41
+ * Optional title displayed as a heading in the card.
42
+ * Can be any React node (string, JSX, etc.).
43
+ */
44
+ title?: ReactNode | undefined;
45
+ /**
46
+ * Visual status of the card, affecting its background color.
47
+ * - `'error'`: Red background (bg-red-100 dark:bg-red-900)
48
+ * - `'warn'`: Yellow background (bg-yellow-100 dark:bg-yellow-900)
49
+ * - `'info'`: Blue background (bg-sky-100 dark:bg-sky-900) - default
50
+ */
51
+ status?: 'error' | 'warn' | 'info' | undefined;
52
+ /**
53
+ * Additional CSS classes or a function to compute classes.
54
+ *
55
+ * If a string is provided, it will be merged with the default classes.
56
+ * If a function is provided, it receives the card state and default className,
57
+ * and should return the final className string.
58
+ */
59
+ className?: ((state: Pick<StoryCardProps, 'status'> & {
60
+ defaultClassName: string;
61
+ }) => string) | ClassValue | undefined;
62
+ /**
63
+ * Content to display in the card body.
64
+ * Can be any React node (string, JSX, etc.).
65
+ *
66
+ * If not provided, the decorator will automatically use:
67
+ * 1. Story description (`parameters.docs.description.story`)
68
+ * 2. Component description (`parameters.docs.description.component`)
69
+ * 3. Nothing (card won't render if no content and no title)
70
+ */
71
+ content?: ReactNode | undefined;
72
+ };
73
+ /**
74
+ * A decorator that adds a card section to display additional information about the story.
75
+ *
76
+ * The card is automatically hidden when the story is shown in docs mode.
77
+ * Multiple decorators can be chained together,
78
+ * and all cards will be collected and displayed above the story content.
79
+ *
80
+ * @returns A Storybook decorator function.
81
+ *
82
+ * @example
83
+ * Basic usage - automatically uses component or story description:
84
+ * ```tsx
85
+ * export const MyStory: Story = {
86
+ * parameters: defineDocsParam({
87
+ * description: {
88
+ * story: 'This description will be shown in the card'
89
+ * }
90
+ * }),
91
+ * decorators: [withStoryCard()]
92
+ * }
93
+ * ```
94
+ *
95
+ * @example
96
+ * With custom content:
97
+ * ```tsx
98
+ * export const MyStory: Story = {
99
+ * decorators: [
100
+ * withStoryCard({
101
+ * content: <p>This is a custom message displayed in the card.</p>
102
+ * })
103
+ * ]
104
+ * }
105
+ * ```
106
+ *
107
+ * @example
108
+ * With title and status:
109
+ * ```tsx
110
+ * export const MyStory: Story = {
111
+ * decorators: [
112
+ * withStoryCard({
113
+ * title: 'Important Notice',
114
+ * status: 'warn',
115
+ * content: <p>Please review this carefully.</p>
116
+ * })
117
+ * ]
118
+ * }
119
+ * ```
120
+ *
121
+ * @example
122
+ * Multiple cards:
123
+ * ```tsx
124
+ * export const MyStory: Story = {
125
+ * decorators: [
126
+ * withStoryCard({ title: 'First Card', status: 'info' }),
127
+ * withStoryCard({ title: 'Second Card', status: 'warn' })
128
+ * ]
129
+ * }
130
+ * ```
131
+ *
132
+ * @remarks
133
+ * - The card will not render if both `content` and `title` are missing.
134
+ * - If `content` is not provided, it will automatically use the story description,
135
+ * or fall back to the component description.
136
+ * - Cards are collected and displayed in the order they are defined in the decorators array.
137
+ */
138
+ declare function withStoryCard<TRenderer extends Renderer = Renderer>({
139
+ title,
140
+ content: contentProp,
141
+ ...rest
142
+ }?: StoryCardProps): DecoratorFunction<TRenderer>;
143
+ //#endregion
35
144
  //#region src/parameters/define_actions_param.d.ts
36
145
  interface ActionsParam {
37
146
  actions: {
@@ -571,4 +680,4 @@ type ExtendStoryObj<TMetaOrCmpOrArgs, S extends StoryObj<TMetaOrCmpOrArgs>, E ex
571
680
  tags?: Array<E['tag'] | (string & {})> | undefined;
572
681
  };
573
682
  //#endregion
574
- export { ActionsParam, BackgroundsParam, DocsParam, ExtendMeta, ExtendStoryObj, GlobalApiBackgroundsParam, GlobalApiViewportParam, LayoutParam, ShowHtml, ShowHtmlProps, SourceProps, StorySortParam, StorybookBuiltInParams, TestParam, Viewport, ViewportParam, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineTestParam, defineViewportParam, showDocSource, whenRunningInTest };
683
+ export { ActionsParam, BackgroundsParam, DocsParam, ExtendMeta, ExtendStoryObj, GlobalApiBackgroundsParam, GlobalApiViewportParam, LayoutParam, ShowHtml, ShowHtmlProps, SourceProps, StoryCardProps, StorySortParam, StorybookBuiltInParams, TestParam, Viewport, ViewportParam, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineTestParam, defineViewportParam, showDocSource, whenRunningInTest, withStoryCard };
package/esm/index.js CHANGED
@@ -1,10 +1,13 @@
1
+
1
2
  import { isRunningInTest } from "@repobuddy/test";
2
3
  import { prettify } from "htmlfy";
3
- import { useEffect, useState } from "react";
4
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  import { SyntaxHighlighter } from "storybook/internal/components";
6
7
  import { addons } from "storybook/preview-api";
7
8
  import { ThemeProvider, convert, themes } from "storybook/theming";
9
+ import { cva } from "class-variance-authority";
10
+ import { twMerge } from "tailwind-merge";
8
11
 
9
12
  export * from "@repobuddy/test"
10
13
 
@@ -67,6 +70,161 @@ function showDocSource() {
67
70
  };
68
71
  }
69
72
 
73
+ //#endregion
74
+ //#region src/decorators/with_story_card.tsx
75
+ /**
76
+ * A decorator that adds a card section to display additional information about the story.
77
+ *
78
+ * The card is automatically hidden when the story is shown in docs mode.
79
+ * Multiple decorators can be chained together,
80
+ * and all cards will be collected and displayed above the story content.
81
+ *
82
+ * @returns A Storybook decorator function.
83
+ *
84
+ * @example
85
+ * Basic usage - automatically uses component or story description:
86
+ * ```tsx
87
+ * export const MyStory: Story = {
88
+ * parameters: defineDocsParam({
89
+ * description: {
90
+ * story: 'This description will be shown in the card'
91
+ * }
92
+ * }),
93
+ * decorators: [withStoryCard()]
94
+ * }
95
+ * ```
96
+ *
97
+ * @example
98
+ * With custom content:
99
+ * ```tsx
100
+ * export const MyStory: Story = {
101
+ * decorators: [
102
+ * withStoryCard({
103
+ * content: <p>This is a custom message displayed in the card.</p>
104
+ * })
105
+ * ]
106
+ * }
107
+ * ```
108
+ *
109
+ * @example
110
+ * With title and status:
111
+ * ```tsx
112
+ * export const MyStory: Story = {
113
+ * decorators: [
114
+ * withStoryCard({
115
+ * title: 'Important Notice',
116
+ * status: 'warn',
117
+ * content: <p>Please review this carefully.</p>
118
+ * })
119
+ * ]
120
+ * }
121
+ * ```
122
+ *
123
+ * @example
124
+ * Multiple cards:
125
+ * ```tsx
126
+ * export const MyStory: Story = {
127
+ * decorators: [
128
+ * withStoryCard({ title: 'First Card', status: 'info' }),
129
+ * withStoryCard({ title: 'Second Card', status: 'warn' })
130
+ * ]
131
+ * }
132
+ * ```
133
+ *
134
+ * @remarks
135
+ * - The card will not render if both `content` and `title` are missing.
136
+ * - If `content` is not provided, it will automatically use the story description,
137
+ * or fall back to the component description.
138
+ * - Cards are collected and displayed in the order they are defined in the decorators array.
139
+ */
140
+ function withStoryCard({ title, content: contentProp, ...rest } = {}) {
141
+ return (Story, { parameters, viewMode }) => {
142
+ if (viewMode === "docs") return /* @__PURE__ */ jsx(Story, {});
143
+ const content = contentProp ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component;
144
+ if (!content && !title) return /* @__PURE__ */ jsx(Story, {});
145
+ return /* @__PURE__ */ jsx(StoryCardContainerWrapper, {
146
+ Story,
147
+ content,
148
+ title,
149
+ ...rest
150
+ });
151
+ };
152
+ }
153
+ function StoryCardContainerWrapper({ Story, ...props }) {
154
+ const context = useContext(StoryCardContext);
155
+ const collector = /* @__PURE__ */ jsx(StoryCardCollector, {
156
+ Story,
157
+ ...props
158
+ });
159
+ if (context === null) return /* @__PURE__ */ jsx(StoryCardContainer, { children: collector });
160
+ return collector;
161
+ }
162
+ const StoryCardContext = createContext(null);
163
+ function StoryCardContainer({ children }) {
164
+ const [cards, setCards] = useState([]);
165
+ const contextValue = useMemo(() => ({
166
+ addCard(card) {
167
+ const id = `story-card-${crypto.randomUUID()}`;
168
+ setCards((cards$1) => [...cards$1, {
169
+ ...card,
170
+ id
171
+ }]);
172
+ return id;
173
+ },
174
+ removeCard(id) {
175
+ setCards((cards$1) => cards$1.filter((card) => card.id !== id));
176
+ }
177
+ }), []);
178
+ return /* @__PURE__ */ jsx(StoryCardContext.Provider, {
179
+ value: contextValue,
180
+ children: /* @__PURE__ */ jsxs("div", {
181
+ className: "flex flex-col gap-2",
182
+ children: [cards.map(({ id, status, className, content, title }) => /* @__PURE__ */ jsxs("section", {
183
+ className: storyCardTheme({ status }, className),
184
+ children: [title && /* @__PURE__ */ jsx("h2", {
185
+ className: "text-lg font-bold",
186
+ children: title
187
+ }), content]
188
+ }, id)), children]
189
+ })
190
+ });
191
+ }
192
+ function StoryCardCollector({ Story, title, status, className, content }) {
193
+ const context = useContext(StoryCardContext);
194
+ const cardIdRef = useRef(null);
195
+ useLayoutEffect(() => {
196
+ if (cardIdRef.current === null) cardIdRef.current = context.addCard({
197
+ title,
198
+ status,
199
+ className,
200
+ content
201
+ });
202
+ return () => {
203
+ if (cardIdRef.current !== null) {
204
+ context.removeCard(cardIdRef.current);
205
+ cardIdRef.current = null;
206
+ }
207
+ };
208
+ }, []);
209
+ return /* @__PURE__ */ jsx(Story, {});
210
+ }
211
+ const storyCardTheme = (state, className) => {
212
+ const defaultClassName = storyCardVariants(state);
213
+ if (!className) return defaultClassName;
214
+ return twMerge(defaultClassName, typeof className === "function" ? className({
215
+ ...state,
216
+ defaultClassName
217
+ }) : className);
218
+ };
219
+ const storyCardVariants = cva("flex flex-col gap-1 py-3 px-4 rounded text-black dark:text-gray-100", {
220
+ variants: { status: {
221
+ error: "bg-red-100 dark:bg-red-900",
222
+ warn: "bg-yellow-100 dark:bg-yellow-900",
223
+ info: "bg-sky-100 dark:bg-sky-900"
224
+ } },
225
+ defaultVariants: { status: "info" }
226
+ });
227
+
70
228
  //#endregion
71
229
  //#region src/parameters/define_actions_param.ts
72
230
  /**
@@ -248,4 +406,4 @@ function whenRunningInTest(decoratorOrHandler) {
248
406
  }
249
407
 
250
408
  //#endregion
251
- export { ShowHtml, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineTestParam, defineViewportParam, showDocSource, whenRunningInTest };
409
+ export { ShowHtml, defineActionsParam, defineBackgroundsParam, defineDocsParam, defineLayoutParam, defineParameters, defineTestParam, defineViewportParam, showDocSource, whenRunningInTest, withStoryCard };
@@ -1,3 +1,4 @@
1
+
1
2
  //#region src/manager/brand_title.d.ts
2
3
  interface BrandTitleOptions {
3
4
  /**
@@ -1,3 +1,4 @@
1
+
1
2
  //#region src/manager/brand_title.ts
2
3
  /**
3
4
  * Creates a brand title element for the Storybook manager UI.
@@ -1,3 +1,4 @@
1
+
1
2
  import { TagBadgeParameters } from "storybook-addon-tag-badges/manager-helpers";
2
3
  import { Args, Meta as Meta$1, StoryObj as StoryObj$1 } from "@storybook/react-vite";
3
4
 
@@ -6,7 +7,7 @@ type TagBadgeParameter = TagBadgeParameters[0];
6
7
  /**
7
8
  * Type representing the names of predefined tags used in Storybook stories.
8
9
  */
9
- type TagNames = 'editor' | 'new' | 'beta' | 'props' | 'deprecated' | 'outdated' | 'danger' | 'todo' | 'code-only' | 'snapshot' | 'unit' | 'integration' | 'keyboard' | 'internal';
10
+ type TagNames = 'editor' | 'new' | 'beta' | 'props' | 'deprecated' | 'outdated' | 'danger' | 'todo' | 'code-only' | 'snapshot' | 'unit' | 'integration' | 'keyboard' | 'internal' | 'usecase';
10
11
  /**
11
12
  * Configuration for story tag badges that appear in the Storybook sidebar.
12
13
  * Each badge is associated with a specific tag and displays an emoji with a tooltip.
@@ -1,3 +1,4 @@
1
+
1
2
  import { defaultConfig } from "storybook-addon-tag-badges/manager-helpers";
2
3
 
3
4
  //#region src/storybook-addon-tag-badges/tag_badges.ts
@@ -1,3 +1,4 @@
1
+
1
2
  import { DocsContextProps } from "@storybook/addon-docs/blocks";
2
3
  import { PropsWithChildren } from "react";
3
4
  import { ThemeVars } from "storybook/theming";
@@ -1,3 +1,4 @@
1
+
1
2
  import { DARK_MODE_EVENT_NAME, useDarkMode } from "@storybook-community/storybook-dark-mode";
2
3
  import { DocsContainer } from "@storybook/addon-docs/blocks";
3
4
  import { useEffect, useState } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repobuddy/storybook",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Storybook repo buddy",
5
5
  "keywords": [
6
6
  "storybook",
@@ -38,44 +38,46 @@
38
38
  "./storybook-dark-mode": {
39
39
  "types": "./esm/storybook-dark-mode/index.d.ts",
40
40
  "default": "./esm/storybook-dark-mode/index.js"
41
- }
41
+ },
42
+ "./styles.css": "./styles.css"
42
43
  },
43
44
  "files": [
44
45
  "cjs",
45
46
  "esm",
46
47
  "src",
48
+ "styles.css",
47
49
  "!**/*.{spec,test,unit,accept,integrate,system,perf,stress,study,stories}.*",
48
50
  "!**/*.mdx"
49
51
  ],
50
52
  "dependencies": {
51
53
  "@just-web/css": "^0.7.0",
52
54
  "@repobuddy/test": "^1.0.0",
53
- "@storybook/icons": "^2.0.1",
54
- "htmlfy": "^1.0.0"
55
+ "class-variance-authority": "^0.7.1",
56
+ "htmlfy": "^1.0.0",
57
+ "tailwind-merge": "^3.4.0"
55
58
  },
56
59
  "devDependencies": {
57
60
  "@repobuddy/vitest": "^2.0.0",
58
61
  "@storybook-community/storybook-dark-mode": "^7.0.2",
59
62
  "@storybook/addon-docs": "^10.0.7",
60
- "@storybook/addon-themes": "^10.0.7",
61
63
  "@storybook/addon-vitest": "^10.0.7",
62
64
  "@storybook/react-vite": "^10.0.7",
63
65
  "@tailwindcss/cli": "^4.1.17",
64
66
  "@tailwindcss/vite": "^4.1.17",
65
- "@vitest/browser": "^4.0.9",
66
- "@vitest/browser-playwright": "^4.0.9",
67
- "@vitest/coverage-v8": "^4.0.9",
67
+ "@vitest/browser": "^4.0.16",
68
+ "@vitest/browser-playwright": "^4.0.16",
69
+ "@vitest/coverage-v8": "^4.0.16",
68
70
  "dedent": "^1.7.0",
69
- "ncp": "^2.0.0",
71
+ "npm-run-all2": "^8.0.4",
70
72
  "react": "^19.2.0",
71
73
  "react-dom": "^19.2.0",
72
74
  "rimraf": "^6.1.0",
73
75
  "storybook": "^10.0.8",
74
76
  "storybook-addon-tag-badges": "^3.0.2",
75
77
  "tailwindcss": "^4.1.17",
76
- "tsdown": "^0.16.6",
77
- "vite": "^7.2.2",
78
- "vitest": "^4.0.9"
78
+ "tsdown": "^0.18.0",
79
+ "vite": "^7.3.0",
80
+ "vitest": "^4.0.16"
79
81
  },
80
82
  "peerDependencies": {
81
83
  "@storybook-community/storybook-dark-mode": "^7.0.0",
@@ -91,7 +93,9 @@
91
93
  }
92
94
  },
93
95
  "scripts": {
94
- "build": "tsdown",
96
+ "build": "run-p build:*",
97
+ "build:code": "tsdown",
98
+ "build:css": "tailwindcss -i ./tailwind.css -o ./styles.css",
95
99
  "clean": "rimraf .turbo coverage cjs esm storybook-static *.tsbuildinfo",
96
100
  "coverage": "vitest run --coverage",
97
101
  "sb": "storybook dev -p 6006",
@@ -100,6 +104,7 @@
100
104
  "test:dark": "vitest run --config=vitest.config.dark.ts",
101
105
  "test:light": "vitest run --config=vitest.config.light.ts",
102
106
  "test:theme": "vitest run --config=vitest.config.theme.ts",
107
+ "test:types": "tsc --noEmit",
103
108
  "w": "vitest"
104
109
  }
105
110
  }
package/readme.md CHANGED
@@ -4,9 +4,11 @@ Your repository buddy for Storybook.
4
4
 
5
5
  > [!NOTE]
6
6
  >
7
- > This package supports Storybook 9.x from version `1.0.0`.
7
+ > For Storybook 10, please use version `2.x`.
8
8
  >
9
- > For Storybook 8.x, please use `0.x` version.
9
+ > For Storybook 9, please use version `1.x`.
10
+ >
11
+ > For Storybook 8.x, please use version `0.x`.
10
12
 
11
13
  ## Install
12
14
 
@@ -14,6 +16,8 @@ Your repository buddy for Storybook.
14
16
  pnpm add -D @repobuddy/storybook
15
17
  ```
16
18
 
19
+ If you use the components in the library, import `@repobuddy/storybook/styles.css`.
20
+
17
21
  ## Features
18
22
 
19
23
  ### Typed Parameters
@@ -0,0 +1,235 @@
1
+ import { cva } from 'class-variance-authority'
2
+ import type { ClassValue } from 'class-variance-authority/types'
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ComponentType,
11
+ type ReactNode
12
+ } from 'react'
13
+ import type { DecoratorFunction, Renderer } from 'storybook/internal/csf'
14
+ import { twMerge } from 'tailwind-merge'
15
+
16
+ export type StoryCardProps = {
17
+ /**
18
+ * Optional title displayed as a heading in the card.
19
+ * Can be any React node (string, JSX, etc.).
20
+ */
21
+ title?: ReactNode | undefined
22
+ /**
23
+ * Visual status of the card, affecting its background color.
24
+ * - `'error'`: Red background (bg-red-100 dark:bg-red-900)
25
+ * - `'warn'`: Yellow background (bg-yellow-100 dark:bg-yellow-900)
26
+ * - `'info'`: Blue background (bg-sky-100 dark:bg-sky-900) - default
27
+ */
28
+ status?: 'error' | 'warn' | 'info' | undefined
29
+ /**
30
+ * Additional CSS classes or a function to compute classes.
31
+ *
32
+ * If a string is provided, it will be merged with the default classes.
33
+ * If a function is provided, it receives the card state and default className,
34
+ * and should return the final className string.
35
+ */
36
+ className?:
37
+ | ((state: Pick<StoryCardProps, 'status'> & { defaultClassName: string }) => string)
38
+ | ClassValue
39
+ | undefined
40
+ /**
41
+ * Content to display in the card body.
42
+ * Can be any React node (string, JSX, etc.).
43
+ *
44
+ * If not provided, the decorator will automatically use:
45
+ * 1. Story description (`parameters.docs.description.story`)
46
+ * 2. Component description (`parameters.docs.description.component`)
47
+ * 3. Nothing (card won't render if no content and no title)
48
+ */
49
+ content?: ReactNode | undefined
50
+ }
51
+
52
+ /**
53
+ * A decorator that adds a card section to display additional information about the story.
54
+ *
55
+ * The card is automatically hidden when the story is shown in docs mode.
56
+ * Multiple decorators can be chained together,
57
+ * and all cards will be collected and displayed above the story content.
58
+ *
59
+ * @returns A Storybook decorator function.
60
+ *
61
+ * @example
62
+ * Basic usage - automatically uses component or story description:
63
+ * ```tsx
64
+ * export const MyStory: Story = {
65
+ * parameters: defineDocsParam({
66
+ * description: {
67
+ * story: 'This description will be shown in the card'
68
+ * }
69
+ * }),
70
+ * decorators: [withStoryCard()]
71
+ * }
72
+ * ```
73
+ *
74
+ * @example
75
+ * With custom content:
76
+ * ```tsx
77
+ * export const MyStory: Story = {
78
+ * decorators: [
79
+ * withStoryCard({
80
+ * content: <p>This is a custom message displayed in the card.</p>
81
+ * })
82
+ * ]
83
+ * }
84
+ * ```
85
+ *
86
+ * @example
87
+ * With title and status:
88
+ * ```tsx
89
+ * export const MyStory: Story = {
90
+ * decorators: [
91
+ * withStoryCard({
92
+ * title: 'Important Notice',
93
+ * status: 'warn',
94
+ * content: <p>Please review this carefully.</p>
95
+ * })
96
+ * ]
97
+ * }
98
+ * ```
99
+ *
100
+ * @example
101
+ * Multiple cards:
102
+ * ```tsx
103
+ * export const MyStory: Story = {
104
+ * decorators: [
105
+ * withStoryCard({ title: 'First Card', status: 'info' }),
106
+ * withStoryCard({ title: 'Second Card', status: 'warn' })
107
+ * ]
108
+ * }
109
+ * ```
110
+ *
111
+ * @remarks
112
+ * - The card will not render if both `content` and `title` are missing.
113
+ * - If `content` is not provided, it will automatically use the story description,
114
+ * or fall back to the component description.
115
+ * - Cards are collected and displayed in the order they are defined in the decorators array.
116
+ */
117
+ export function withStoryCard<TRenderer extends Renderer = Renderer>({
118
+ title,
119
+ content: contentProp,
120
+ ...rest
121
+ }: StoryCardProps = {}): DecoratorFunction<TRenderer> {
122
+ return (Story, { parameters, viewMode }) => {
123
+ if (viewMode === 'docs') return <Story />
124
+
125
+ const content = contentProp ?? parameters.docs?.description?.story ?? parameters.docs?.description?.component
126
+ if (!content && !title) return <Story />
127
+
128
+ return <StoryCardContainerWrapper Story={Story} content={content} title={title} {...rest} />
129
+ }
130
+ }
131
+
132
+ interface StoryCardContainerWrapperProps extends StoryCardProps {
133
+ Story: ComponentType
134
+ }
135
+
136
+ function StoryCardContainerWrapper({ Story, ...props }: StoryCardContainerWrapperProps) {
137
+ const context = useContext(StoryCardContext)
138
+ const collector = <StoryCardCollector Story={Story} {...props} />
139
+
140
+ if (context === null) {
141
+ return <StoryCardContainer>{collector}</StoryCardContainer>
142
+ }
143
+
144
+ return collector
145
+ }
146
+
147
+ interface StoryCardContextValue {
148
+ addCard: (card: StoryCardProps) => string
149
+ removeCard: (id: string) => void
150
+ }
151
+
152
+ const StoryCardContext = createContext<StoryCardContextValue | null>(null)
153
+
154
+ type StoryCardWithId = StoryCardProps & { id: string }
155
+
156
+ function StoryCardContainer({ children }: { children: ReactNode }) {
157
+ const [cards, setCards] = useState<StoryCardWithId[]>([])
158
+
159
+ const contextValue: StoryCardContextValue = useMemo(
160
+ () => ({
161
+ addCard(card) {
162
+ const id = `story-card-${crypto.randomUUID()}`
163
+ setCards((cards) => [...cards, { ...card, id }])
164
+ return id
165
+ },
166
+ removeCard(id) {
167
+ setCards((cards) => cards.filter((card) => card.id !== id))
168
+ }
169
+ }),
170
+ []
171
+ )
172
+
173
+ return (
174
+ <StoryCardContext.Provider value={contextValue}>
175
+ <div className="flex flex-col gap-2">
176
+ {cards.map(({ id, status, className, content, title }) => (
177
+ <section key={id} className={storyCardTheme({ status }, className)}>
178
+ {title && <h2 className="text-lg font-bold">{title}</h2>}
179
+ {content}
180
+ </section>
181
+ ))}
182
+ {children}
183
+ </div>
184
+ </StoryCardContext.Provider>
185
+ )
186
+ }
187
+
188
+ interface StoryCardCollectorProps extends StoryCardProps {
189
+ Story: ComponentType
190
+ }
191
+
192
+ function StoryCardCollector({ Story, title, status, className, content }: StoryCardCollectorProps) {
193
+ // StoryCardCollector is an internal component. Context is guaranteed to be not null by `StoryCardContainer`.
194
+ const context = useContext(StoryCardContext)!
195
+ const cardIdRef = useRef<string | null>(null)
196
+
197
+ // Collect this card once into the collection
198
+ useLayoutEffect(() => {
199
+ // Only add if not already added (handles Strict Mode double-render)
200
+ if (cardIdRef.current === null) {
201
+ cardIdRef.current = context.addCard({ title, status, className, content })
202
+ }
203
+
204
+ return () => {
205
+ if (cardIdRef.current !== null) {
206
+ context.removeCard(cardIdRef.current)
207
+ cardIdRef.current = null
208
+ }
209
+ }
210
+ }, [])
211
+
212
+ return <Story />
213
+ }
214
+
215
+ const storyCardTheme = (state: Pick<StoryCardProps, 'status'>, className: StoryCardProps['className']) => {
216
+ const defaultClassName = storyCardVariants(state)
217
+ if (!className) return defaultClassName
218
+ return twMerge(
219
+ defaultClassName,
220
+ typeof className === 'function' ? className({ ...state, defaultClassName }) : className
221
+ )
222
+ }
223
+
224
+ const storyCardVariants = cva('flex flex-col gap-1 py-3 px-4 rounded text-black dark:text-gray-100', {
225
+ variants: {
226
+ status: {
227
+ error: 'bg-red-100 dark:bg-red-900',
228
+ warn: 'bg-yellow-100 dark:bg-yellow-900',
229
+ info: 'bg-sky-100 dark:bg-sky-900'
230
+ }
231
+ },
232
+ defaultVariants: {
233
+ status: 'info'
234
+ }
235
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from '@repobuddy/test'
2
2
  export * from './components/show_html.tsx'
3
3
  export * from './decorators/show_doc_source.tsx'
4
+ export * from './decorators/with_story_card.tsx'
4
5
  export * from './parameters/define_actions_param.ts'
5
6
  export * from './parameters/define_backgrounds_param.ts'
6
7
  export * from './parameters/define_docs_param.ts'
@@ -22,6 +22,7 @@ export type TagNames =
22
22
  | 'integration'
23
23
  | 'keyboard'
24
24
  | 'internal'
25
+ | 'usecase'
25
26
 
26
27
  /**
27
28
  * Configuration for story tag badges that appear in the Storybook sidebar.
package/styles.css ADDED
@@ -0,0 +1,344 @@
1
+ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
2
+ @layer properties;
3
+ @layer theme, base, components, utilities;
4
+ @layer theme {
5
+ :root, :host {
6
+ --color-red-100: oklch(93.6% 0.032 17.717);
7
+ --color-red-800: oklch(44.4% 0.177 26.899);
8
+ --color-red-900: oklch(39.6% 0.141 25.723);
9
+ --color-amber-300: oklch(87.9% 0.169 91.605);
10
+ --color-amber-900: oklch(41.4% 0.112 45.904);
11
+ --color-yellow-100: oklch(97.3% 0.071 103.193);
12
+ --color-yellow-900: oklch(42.1% 0.095 57.708);
13
+ --color-green-200: oklch(92.5% 0.084 155.995);
14
+ --color-green-800: oklch(44.8% 0.119 151.328);
15
+ --color-sky-100: oklch(95.1% 0.026 236.824);
16
+ --color-sky-500: oklch(68.5% 0.169 237.323);
17
+ --color-sky-900: oklch(39.1% 0.09 240.876);
18
+ --color-blue-500: oklch(62.3% 0.214 259.815);
19
+ --color-rose-400: oklch(71.2% 0.194 13.428);
20
+ --color-rose-900: oklch(41% 0.159 10.272);
21
+ --color-gray-100: oklch(96.7% 0.003 264.542);
22
+ --color-gray-500: oklch(55.1% 0.027 264.364);
23
+ --color-black: #000;
24
+ --color-white: #fff;
25
+ --spacing: 0.25rem;
26
+ --text-lg: 1.125rem;
27
+ --text-lg--line-height: calc(1.75 / 1.125);
28
+ --font-weight-extralight: 200;
29
+ --font-weight-bold: 700;
30
+ --font-weight-extrabold: 800;
31
+ }
32
+ }
33
+ @layer utilities {
34
+ .static {
35
+ position: static;
36
+ }
37
+ .container {
38
+ width: 100%;
39
+ @media (width >= 40rem) {
40
+ max-width: 40rem;
41
+ }
42
+ @media (width >= 48rem) {
43
+ max-width: 48rem;
44
+ }
45
+ @media (width >= 64rem) {
46
+ max-width: 64rem;
47
+ }
48
+ @media (width >= 80rem) {
49
+ max-width: 80rem;
50
+ }
51
+ @media (width >= 96rem) {
52
+ max-width: 96rem;
53
+ }
54
+ }
55
+ .flex {
56
+ display: flex;
57
+ }
58
+ .hidden {
59
+ display: none;
60
+ }
61
+ .inline {
62
+ display: inline;
63
+ }
64
+ .transform {
65
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
66
+ }
67
+ .flex-col {
68
+ flex-direction: column;
69
+ }
70
+ .gap-1 {
71
+ gap: calc(var(--spacing) * 1);
72
+ }
73
+ .gap-2 {
74
+ gap: calc(var(--spacing) * 2);
75
+ }
76
+ .gap-4 {
77
+ gap: calc(var(--spacing) * 4);
78
+ }
79
+ .rounded {
80
+ border-radius: 0.25rem;
81
+ }
82
+ .border {
83
+ border-style: var(--tw-border-style);
84
+ border-width: 1px;
85
+ }
86
+ .border-2 {
87
+ border-style: var(--tw-border-style);
88
+ border-width: 2px;
89
+ }
90
+ .border-blue-500 {
91
+ border-color: var(--color-blue-500);
92
+ }
93
+ .bg-amber-300 {
94
+ background-color: var(--color-amber-300);
95
+ }
96
+ .bg-black {
97
+ background-color: var(--color-black);
98
+ }
99
+ .bg-gray-100 {
100
+ background-color: var(--color-gray-100);
101
+ }
102
+ .bg-green-200 {
103
+ background-color: var(--color-green-200);
104
+ }
105
+ .bg-green-800 {
106
+ background-color: var(--color-green-800);
107
+ }
108
+ .bg-red-100 {
109
+ background-color: var(--color-red-100);
110
+ }
111
+ .bg-red-800 {
112
+ background-color: var(--color-red-800);
113
+ }
114
+ .bg-rose-400 {
115
+ background-color: var(--color-rose-400);
116
+ }
117
+ .bg-sky-100 {
118
+ background-color: var(--color-sky-100);
119
+ }
120
+ .bg-sky-500 {
121
+ background-color: var(--color-sky-500);
122
+ }
123
+ .bg-white {
124
+ background-color: var(--color-white);
125
+ }
126
+ .bg-yellow-100 {
127
+ background-color: var(--color-yellow-100);
128
+ }
129
+ .p-2 {
130
+ padding: calc(var(--spacing) * 2);
131
+ }
132
+ .p-4 {
133
+ padding: calc(var(--spacing) * 4);
134
+ }
135
+ .px-4 {
136
+ padding-inline: calc(var(--spacing) * 4);
137
+ }
138
+ .py-3 {
139
+ padding-block: calc(var(--spacing) * 3);
140
+ }
141
+ .text-lg {
142
+ font-size: var(--text-lg);
143
+ line-height: var(--tw-leading, var(--text-lg--line-height));
144
+ }
145
+ .font-bold {
146
+ --tw-font-weight: var(--font-weight-bold);
147
+ font-weight: var(--font-weight-bold);
148
+ }
149
+ .font-extrabold {
150
+ --tw-font-weight: var(--font-weight-extrabold);
151
+ font-weight: var(--font-weight-extrabold);
152
+ }
153
+ .font-extralight {
154
+ --tw-font-weight: var(--font-weight-extralight);
155
+ font-weight: var(--font-weight-extralight);
156
+ }
157
+ .text-black {
158
+ color: var(--color-black);
159
+ }
160
+ .text-white {
161
+ color: var(--color-white);
162
+ }
163
+ .shadow-lg {
164
+ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
165
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
166
+ }
167
+ .dark\:bg-amber-900 {
168
+ &:where(.dark, .dark *) {
169
+ background-color: var(--color-amber-900);
170
+ }
171
+ }
172
+ .dark\:bg-black {
173
+ &:where(.dark, .dark *) {
174
+ background-color: var(--color-black);
175
+ }
176
+ }
177
+ .dark\:bg-gray-500 {
178
+ &:where(.dark, .dark *) {
179
+ background-color: var(--color-gray-500);
180
+ }
181
+ }
182
+ .dark\:bg-green-800 {
183
+ &:where(.dark, .dark *) {
184
+ background-color: var(--color-green-800);
185
+ }
186
+ }
187
+ .dark\:bg-red-900 {
188
+ &:where(.dark, .dark *) {
189
+ background-color: var(--color-red-900);
190
+ }
191
+ }
192
+ .dark\:bg-rose-900 {
193
+ &:where(.dark, .dark *) {
194
+ background-color: var(--color-rose-900);
195
+ }
196
+ }
197
+ .dark\:bg-sky-900 {
198
+ &:where(.dark, .dark *) {
199
+ background-color: var(--color-sky-900);
200
+ }
201
+ }
202
+ .dark\:bg-yellow-900 {
203
+ &:where(.dark, .dark *) {
204
+ background-color: var(--color-yellow-900);
205
+ }
206
+ }
207
+ .dark\:font-extrabold {
208
+ &:where(.dark, .dark *) {
209
+ --tw-font-weight: var(--font-weight-extrabold);
210
+ font-weight: var(--font-weight-extrabold);
211
+ }
212
+ }
213
+ .dark\:text-gray-100 {
214
+ &:where(.dark, .dark *) {
215
+ color: var(--color-gray-100);
216
+ }
217
+ }
218
+ .dark\:text-white {
219
+ &:where(.dark, .dark *) {
220
+ color: var(--color-white);
221
+ }
222
+ }
223
+ }
224
+ @property --tw-rotate-x {
225
+ syntax: "*";
226
+ inherits: false;
227
+ }
228
+ @property --tw-rotate-y {
229
+ syntax: "*";
230
+ inherits: false;
231
+ }
232
+ @property --tw-rotate-z {
233
+ syntax: "*";
234
+ inherits: false;
235
+ }
236
+ @property --tw-skew-x {
237
+ syntax: "*";
238
+ inherits: false;
239
+ }
240
+ @property --tw-skew-y {
241
+ syntax: "*";
242
+ inherits: false;
243
+ }
244
+ @property --tw-border-style {
245
+ syntax: "*";
246
+ inherits: false;
247
+ initial-value: solid;
248
+ }
249
+ @property --tw-font-weight {
250
+ syntax: "*";
251
+ inherits: false;
252
+ }
253
+ @property --tw-shadow {
254
+ syntax: "*";
255
+ inherits: false;
256
+ initial-value: 0 0 #0000;
257
+ }
258
+ @property --tw-shadow-color {
259
+ syntax: "*";
260
+ inherits: false;
261
+ }
262
+ @property --tw-shadow-alpha {
263
+ syntax: "<percentage>";
264
+ inherits: false;
265
+ initial-value: 100%;
266
+ }
267
+ @property --tw-inset-shadow {
268
+ syntax: "*";
269
+ inherits: false;
270
+ initial-value: 0 0 #0000;
271
+ }
272
+ @property --tw-inset-shadow-color {
273
+ syntax: "*";
274
+ inherits: false;
275
+ }
276
+ @property --tw-inset-shadow-alpha {
277
+ syntax: "<percentage>";
278
+ inherits: false;
279
+ initial-value: 100%;
280
+ }
281
+ @property --tw-ring-color {
282
+ syntax: "*";
283
+ inherits: false;
284
+ }
285
+ @property --tw-ring-shadow {
286
+ syntax: "*";
287
+ inherits: false;
288
+ initial-value: 0 0 #0000;
289
+ }
290
+ @property --tw-inset-ring-color {
291
+ syntax: "*";
292
+ inherits: false;
293
+ }
294
+ @property --tw-inset-ring-shadow {
295
+ syntax: "*";
296
+ inherits: false;
297
+ initial-value: 0 0 #0000;
298
+ }
299
+ @property --tw-ring-inset {
300
+ syntax: "*";
301
+ inherits: false;
302
+ }
303
+ @property --tw-ring-offset-width {
304
+ syntax: "<length>";
305
+ inherits: false;
306
+ initial-value: 0px;
307
+ }
308
+ @property --tw-ring-offset-color {
309
+ syntax: "*";
310
+ inherits: false;
311
+ initial-value: #fff;
312
+ }
313
+ @property --tw-ring-offset-shadow {
314
+ syntax: "*";
315
+ inherits: false;
316
+ initial-value: 0 0 #0000;
317
+ }
318
+ @layer properties {
319
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
320
+ *, ::before, ::after, ::backdrop {
321
+ --tw-rotate-x: initial;
322
+ --tw-rotate-y: initial;
323
+ --tw-rotate-z: initial;
324
+ --tw-skew-x: initial;
325
+ --tw-skew-y: initial;
326
+ --tw-border-style: solid;
327
+ --tw-font-weight: initial;
328
+ --tw-shadow: 0 0 #0000;
329
+ --tw-shadow-color: initial;
330
+ --tw-shadow-alpha: 100%;
331
+ --tw-inset-shadow: 0 0 #0000;
332
+ --tw-inset-shadow-color: initial;
333
+ --tw-inset-shadow-alpha: 100%;
334
+ --tw-ring-color: initial;
335
+ --tw-ring-shadow: 0 0 #0000;
336
+ --tw-inset-ring-color: initial;
337
+ --tw-inset-ring-shadow: 0 0 #0000;
338
+ --tw-ring-inset: initial;
339
+ --tw-ring-offset-width: 0px;
340
+ --tw-ring-offset-color: #fff;
341
+ --tw-ring-offset-shadow: 0 0 #0000;
342
+ }
343
+ }
344
+ }
@@ -1,3 +0,0 @@
1
- console.info('variants.index.ts.load')
2
-
3
- export const empty = {}
@@ -1,3 +0,0 @@
1
- {
2
- "type": "module"
3
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * User specifies `@repobuddy/storybook/variants/addon` in the `.storybook/main.ts`.
3
- *
4
- * Storybook loads it on the server side.
5
- *
6
- * It provides a default export (or in CJS, `module.exports =`) that contains:
7
- *
8
- * - `managerEntries` a function that returns an array of strings on where to load the manager (on the client side)
9
- * - `previewAnnotations` a function that returns an array of strings on where to load the preview (on the client side)
10
- * - `experimental_serverChannel` a function that handles the server-side channel
11
- */
12
-
13
- console.info('variants.preset.js.load')
14
-
15
- // export const managerEntries = ['@storybook-community/storybook-dark-mode/manager']
16
- // const previewAnnotations = (entry = []) => [...entry, require.resolve('@storybook-community/storybook-dark-mode')]
17
- // const previewAnnotations = [ require.resolve('@storybook-community/storybook-dark-mode')]
18
-
19
- // module.exports = {
20
- // managerEntries,
21
- // // previewAnnotations
22
- // }