@gtkx/storybook 0.0.1 → 2.0.0-beta.12

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/src/controls.tsx CHANGED
@@ -7,6 +7,9 @@ import type { Args, ArgTypes } from "./types.js";
7
7
 
8
8
  type ControlsProps = { argTypes: ArgTypes; args: Args; onChange: (update: Args) => void };
9
9
 
10
+ const NO_SELECTION_ID = "no-selection";
11
+ const NO_SELECTION_LABEL = "Choose option...";
12
+
10
13
  type ControlProps = {
11
14
  argument: string;
12
15
  title: string;
@@ -24,7 +27,7 @@ const isObject = (value: unknown): value is Args =>
24
27
  const isWithinBoundsOrAbsent = (value: unknown, min: number, max: number): boolean =>
25
28
  value === undefined || (typeof value === "number" && Number.isFinite(value) && value >= min && value <= max);
26
29
 
27
- const controlTitle = (name: unknown, argument: string): string => typeof name === "string" ? name : argument;
30
+ const controlTitle = (name: unknown, argument: string): string => (typeof name === "string" ? name : argument);
28
31
 
29
32
  const numberDigits = (step: number): number => {
30
33
  const [coefficient = "", exponent = "0"] = String(step).split("e", 2);
@@ -51,9 +54,15 @@ const NumberControl = ({
51
54
  const step = settings.step ?? 1;
52
55
 
53
56
  if (
54
- typeof min !== "number" || typeof max !== "number" || typeof step !== "number" ||
55
- !Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(step) ||
56
- step <= 0 || min > max || !isWithinBoundsOrAbsent(value, min, max)
57
+ typeof min !== "number" ||
58
+ typeof max !== "number" ||
59
+ typeof step !== "number" ||
60
+ !Number.isFinite(min) ||
61
+ !Number.isFinite(max) ||
62
+ !Number.isFinite(step) ||
63
+ step <= 0 ||
64
+ min > max ||
65
+ !isWithinBoundsOrAbsent(value, min, max)
57
66
  ) {
58
67
  return <UnsupportedControl title={title} description="Provide a finite number and valid bounds." />;
59
68
  }
@@ -69,7 +78,7 @@ const NumberControl = ({
69
78
  digits={numberDigits(step)}
70
79
  numeric
71
80
  widthChars={8}
72
- adjustment={(
81
+ adjustment={
73
82
  <GtkAdjustment
74
83
  lower={min}
75
84
  upper={max}
@@ -77,12 +86,9 @@ const NumberControl = ({
77
86
  pageIncrement={Math.min(Number.MAX_VALUE, step * 10)}
78
87
  value={currentValue}
79
88
  />
80
- )}
89
+ }
81
90
  onNotifyValue={(next) => {
82
- if (
83
- typeof next === "number" && Number.isFinite(next) &&
84
- next >= min && next <= max && !Object.is(next, value)
85
- ) {
91
+ if (!Object.is(next, value)) {
86
92
  onChange({ [argument]: next });
87
93
  }
88
94
  }}
@@ -92,19 +98,31 @@ const NumberControl = ({
92
98
 
93
99
  const SelectControl = (props: Omit<ControlProps, "type">): ReactNode => {
94
100
  const { argument, title, value, options, settings, isDisabled, onChange } = props;
95
- if (!Array.isArray(options) || options.length === 0 || options.some((option: unknown) =>
96
- option !== null && typeof option !== "string" && typeof option !== "number" && typeof option !== "boolean")) {
101
+ if (
102
+ !Array.isArray(options) ||
103
+ options.length === 0 ||
104
+ options.some(
105
+ (option: unknown) =>
106
+ option !== null &&
107
+ typeof option !== "string" &&
108
+ typeof option !== "number" &&
109
+ typeof option !== "boolean",
110
+ )
111
+ ) {
97
112
  return <UnsupportedControl title={title} description="Provide a list of text, number, or boolean options." />;
98
113
  }
99
114
 
100
115
  const values: unknown[] = options;
101
116
  const selected = values.findIndex((option) => Object.is(option, value));
102
117
  const labels = isObject(settings.labels) ? settings.labels : {};
103
- const items = values.map((option, index) => {
104
- const label = labels[String(option)];
118
+ const items = [
119
+ { id: NO_SELECTION_ID, value: NO_SELECTION_LABEL },
120
+ ...values.map((option, index) => {
121
+ const label = labels[String(option)];
105
122
 
106
- return { id: String(index), value: typeof label === "string" ? label : String(option) };
107
- });
123
+ return { id: String(index), value: typeof label === "string" ? label : String(option) };
124
+ }),
125
+ ];
108
126
 
109
127
  return (
110
128
  <ComboRow
@@ -112,13 +130,19 @@ const SelectControl = (props: Omit<ControlProps, "type">): ReactNode => {
112
130
  title={markupEscapeText(title, -1)}
113
131
  sensitive={!isDisabled}
114
132
  items={items}
115
- selectedId={selected === -1 ? null : String(selected)}
133
+ selectedId={selected === -1 ? NO_SELECTION_ID : String(selected)}
116
134
  onSelectionChanged={(id) => {
117
- const index = Number(id);
135
+ if (id === NO_SELECTION_ID) {
136
+ onChange({ [argument]: undefined });
118
137
 
119
- if (Number.isSafeInteger(index) && index >= 0 && index < values.length) {
120
- onChange({ [argument]: values[index] });
138
+ return;
121
139
  }
140
+
141
+ if (id === null) {
142
+ return;
143
+ }
144
+
145
+ onChange({ [argument]: values[Number(id)] });
122
146
  }}
123
147
  />
124
148
  );
@@ -136,7 +160,7 @@ const Control = (props: ControlProps): ReactNode => {
136
160
  sensitive={!isDisabled}
137
161
  active={value === true}
138
162
  onNotifyActive={(active) => {
139
- if (typeof active === "boolean" && !Object.is(active, value)) {
163
+ if (!Object.is(active, value)) {
140
164
  onChange({ [argument]: active });
141
165
  }
142
166
  }}
@@ -151,7 +175,7 @@ const Control = (props: ControlProps): ReactNode => {
151
175
  sensitive={!isDisabled}
152
176
  text={typeof value === "string" ? value : ""}
153
177
  onNotifyText={(text) => {
154
- if (typeof text === "string" && !Object.is(text, value)) {
178
+ if (!Object.is(text, value)) {
155
179
  onChange({ [argument]: text });
156
180
  }
157
181
  }}
@@ -177,9 +201,9 @@ const Controls = ({ argTypes, args, onChange }: ControlsProps): ReactNode => (
177
201
  <AdwPreferencesGroup title="Controls">
178
202
  {Object.entries(argTypes).map(([argument, annotation]) => {
179
203
  const control: unknown = annotation?.control;
180
- const table: unknown = annotation?.table;
204
+ const table = isObject(annotation?.table) ? annotation.table : {};
181
205
 
182
- if (control === undefined || control === false || (isObject(table) && table.disable === true)) {
206
+ if (control === undefined || control === false || table.disable === true) {
183
207
  return null;
184
208
  }
185
209
 
@@ -195,7 +219,7 @@ const Controls = ({ argTypes, args, onChange }: ControlsProps): ReactNode => (
195
219
  type={typeof type === "string" ? type : "unknown"}
196
220
  settings={settings}
197
221
  options={annotation?.options}
198
- isDisabled={settings.disable === true}
222
+ isDisabled={settings.disable === true || table.readonly === true}
199
223
  onChange={onChange}
200
224
  />
201
225
  );
@@ -20,14 +20,7 @@ type GroupProps = Pick<NavigatorProps, "selectedId" | "onSelect"> & {
20
20
 
21
21
  const StoryGroup = ({ title, entries, selectedId, onSelect }: GroupProps): ReactNode => (
22
22
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={6}>
23
- <GtkLabel
24
- label={title}
25
- xalign={0}
26
- marginStart={18}
27
- marginEnd={18}
28
- cssClasses={["heading"]}
29
- wrap
30
- />
23
+ <GtkLabel label={title} xalign={0} marginStart={18} marginEnd={18} cssClasses={["heading"]} wrap />
31
24
  <GtkListBox
32
25
  cssClasses={["navigation-sidebar"]}
33
26
  selectedIndex={entries.findIndex((entry) => entry.id === selectedId)}
@@ -54,22 +47,23 @@ const StoryGroup = ({ title, entries, selectedId, onSelect }: GroupProps): React
54
47
  </GtkBox>
55
48
  );
56
49
 
57
- const LoadErrors = ({ errors }: Pick<NavigatorProps, "errors">): ReactNode => errors.length > 0 && (
58
- <GtkBox name="storybook-load-errors" orientation={Gtk.Orientation.VERTICAL} spacing={6}>
59
- {errors.map(({ source, error }, index) => (
60
- <GtkLabel
61
- key={`${source}-${String(index)}`}
62
- label={`${source}\n${error.message.slice(0, 500)}`}
63
- cssClasses={["error"]}
64
- marginStart={18}
65
- marginEnd={18}
66
- wrap
67
- selectable
68
- xalign={0}
69
- />
70
- ))}
71
- </GtkBox>
72
- );
50
+ const LoadErrors = ({ errors }: Pick<NavigatorProps, "errors">): ReactNode =>
51
+ errors.length > 0 && (
52
+ <GtkBox name="storybook-load-errors" orientation={Gtk.Orientation.VERTICAL} spacing={6}>
53
+ {errors.map(({ source, error }, index) => (
54
+ <GtkLabel
55
+ key={`${source}-${String(index)}`}
56
+ label={`${source}\n${error.message.slice(0, 500)}`}
57
+ cssClasses={["error"]}
58
+ marginStart={18}
59
+ marginEnd={18}
60
+ wrap
61
+ selectable
62
+ xalign={0}
63
+ />
64
+ ))}
65
+ </GtkBox>
66
+ );
73
67
 
74
68
  const StoryNavigator = ({ stories, errors, selectedId, onSelect }: NavigatorProps): ReactNode => {
75
69
  const [search, setSearch] = useState("");
@@ -4,6 +4,7 @@ import * as Gtk from "@gtkx/gi/gtk";
4
4
  import { AdwHeaderBar, AdwStatusPage, AdwToolbarView, AdwWindowTitle } from "@gtkx/jsx/adw";
5
5
  import { GtkBox, GtkButton, GtkScrolledWindow } from "@gtkx/jsx/gtk";
6
6
  import { createPortal, rootElement } from "@gtkx/react";
7
+ import { normalizeError } from "@gtkx/utils";
7
8
  import { Component, useCallback, useMemo, useState } from "react";
8
9
  import type { StoryEntry } from "./catalog.js";
9
10
  import type { Args, ComposedStory } from "./types.js";
@@ -32,23 +33,25 @@ class PreviewBoundary extends Component<BoundaryProps, BoundaryState> {
32
33
  }
33
34
 
34
35
  static getDerivedStateFromError(error: unknown): Pick<BoundaryState, "error"> {
35
- return { error: error instanceof Error ? error : new Error(String(error)) };
36
+ return { error: normalizeError(error) };
36
37
  }
37
38
 
38
39
  override state: BoundaryState = { error: null, args: this.props.args };
39
40
 
40
41
  override render(): ReactNode {
41
- return this.state.error === null
42
- ? this.props.children
43
- : (
44
- <AdwStatusPage
45
- name="storybook-preview-error"
46
- title="Preview failed"
47
- iconName="dialog-error-symbolic"
48
- description={markupEscapeText(this.state.error.message.slice(0, 500), -1) +
49
- "\n\nAdjust controls, reset the preview, or select another story to continue."}
50
- />
51
- );
42
+ return this.state.error === null ? (
43
+ this.props.children
44
+ ) : (
45
+ <AdwStatusPage
46
+ name="storybook-preview-error"
47
+ title="Preview failed"
48
+ iconName="dialog-error-symbolic"
49
+ description={
50
+ markupEscapeText(this.state.error.message.slice(0, 500), -1) +
51
+ "\n\nAdjust controls, reset the preview, or select another story to continue."
52
+ }
53
+ />
54
+ );
52
55
  }
53
56
  }
54
57
 
@@ -108,11 +111,16 @@ const usePreviewState = ({ entry }: PreviewProps) => {
108
111
  error: null,
109
112
  actions: new ActionStore(),
110
113
  }));
111
- const onError = useCallback((error: Error) => {
112
- setState((current) => current.story === state.story && current.generation === state.generation
113
- ? { ...current, error }
114
- : current);
115
- }, [state.story, state.generation]);
114
+ const onError = useCallback(
115
+ (error: Error) => {
116
+ setState((current) =>
117
+ current.story === state.story && current.generation === state.generation
118
+ ? { ...current, error }
119
+ : current,
120
+ );
121
+ },
122
+ [state.story, state.generation],
123
+ );
116
124
  const boundArgs = useMemo(
117
125
  () => bindActions(state.args, state.story.argTypes, state.actions, onError),
118
126
  [state.args, state.story.argTypes, state.actions, onError],
@@ -148,12 +156,7 @@ const usePreviewState = ({ entry }: PreviewProps) => {
148
156
  type InspectorProps = PreviewProps & { actions: ActionStore; args: Args; onChange: (args: Args) => void };
149
157
 
150
158
  const StoryInspector = ({ entry, actions, args, onChange }: InspectorProps): ReactNode => (
151
- <GtkScrolledWindow
152
- name="storybook-inspector"
153
- widthRequest={280}
154
- hscrollbarPolicy={Gtk.PolicyType.NEVER}
155
- vexpand
156
- >
159
+ <GtkScrolledWindow name="storybook-inspector" widthRequest={280} hscrollbarPolicy={Gtk.PolicyType.NEVER} vexpand>
157
160
  <GtkBox
158
161
  orientation={Gtk.Orientation.VERTICAL}
159
162
  spacing={24}
@@ -175,27 +178,29 @@ const StorybookPreview = ({ entry }: PreviewProps): ReactNode => {
175
178
  <AdwToolbarView
176
179
  hexpand
177
180
  vexpand
178
- topBar={(
181
+ topBar={
179
182
  <AdwHeaderBar
180
183
  titleWidget={<AdwWindowTitle title={entry.name} subtitle={entry.title} />}
181
184
  end={<GtkButton label="Reset story" onClicked={reset} />}
182
185
  />
183
- )}
186
+ }
184
187
  >
185
188
  <GtkBox>
186
189
  <GtkBox orientation={Gtk.Orientation.VERTICAL} hexpand vexpand>
187
190
  <PreviewBoundary key={state.generation} args={state.args}>
188
- {state.error === null
189
- ? <PreviewContent story={entry.story} args={boundArgs} />
190
- : (
191
- <AdwStatusPage
192
- name="storybook-preview-error"
193
- title="Story action failed"
194
- iconName="dialog-error-symbolic"
195
- description={markupEscapeText(state.error.message.slice(0, 500), -1) +
196
- "\n\nReset the preview or select another story to continue."}
197
- />
198
- )}
191
+ {state.error === null ? (
192
+ <PreviewContent story={entry.story} args={boundArgs} />
193
+ ) : (
194
+ <AdwStatusPage
195
+ name="storybook-preview-error"
196
+ title="Story action failed"
197
+ iconName="dialog-error-symbolic"
198
+ description={
199
+ markupEscapeText(state.error.message.slice(0, 500), -1) +
200
+ "\n\nReset the preview or select another story to continue."
201
+ }
202
+ />
203
+ )}
199
204
  </PreviewBoundary>
200
205
  </GtkBox>
201
206
  <StoryInspector entry={entry} actions={state.actions} args={state.args} onChange={onChange} />
@@ -42,15 +42,15 @@ const StorybookView = ({ catalog, title = "GTKX Storybook" }: StorybookViewProps
42
42
  <AdwOverlaySplitView
43
43
  minSidebarWidth={220}
44
44
  maxSidebarWidth={300}
45
- sidebar={(
45
+ sidebar={
46
46
  <AdwToolbarView
47
- topBar={(
47
+ topBar={
48
48
  <AdwHeaderBar
49
- titleWidget={(
49
+ titleWidget={
50
50
  <AdwWindowTitle title={title} subtitle={`${String(snapshot.stories.length)} stories`} />
51
- )}
51
+ }
52
52
  />
53
- )}
53
+ }
54
54
  >
55
55
  <StoryNavigator
56
56
  stories={snapshot.stories}
@@ -59,20 +59,20 @@ const StorybookView = ({ catalog, title = "GTKX Storybook" }: StorybookViewProps
59
59
  onSelect={setSelectedId}
60
60
  />
61
61
  </AdwToolbarView>
62
- )}
62
+ }
63
63
  >
64
- {selected
65
- ? <StorybookPreview key={selected.id} entry={selected} />
66
- : (
67
- <AdwToolbarView topBar={<AdwHeaderBar />}>
68
- <AdwStatusPage
69
- name="storybook-empty"
70
- title={snapshot.isLoading ? "Loading stories" : "Choose a story"}
71
- iconName="applications-development-symbolic"
72
- description="Stories from your project appear in the sidebar."
73
- />
74
- </AdwToolbarView>
75
- )}
64
+ {selected ? (
65
+ <StorybookPreview key={selected.id} entry={selected} />
66
+ ) : (
67
+ <AdwToolbarView topBar={<AdwHeaderBar />}>
68
+ <AdwStatusPage
69
+ name="storybook-empty"
70
+ title={snapshot.isLoading ? "Loading stories" : "Choose a story"}
71
+ iconName="applications-development-symbolic"
72
+ description="Stories from your project appear in the sidebar."
73
+ />
74
+ </AdwToolbarView>
75
+ )}
76
76
  </AdwOverlaySplitView>
77
77
  );
78
78
  };
package/src/types.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
2
  import type { ComponentType, ReactNode } from "react";
3
3
  import type {
4
+ ArgsFromMeta,
4
5
  ComponentAnnotations,
5
6
  GlobalTypes,
6
7
  Renderer,
8
+ StoryAnnotations as StorybookAnnotations,
7
9
  ArgTypes as StorybookArgTypes,
8
10
  StoryContext as StorybookContext,
9
11
  } from "storybook/internal/types";
@@ -69,51 +71,29 @@ type Annotations<TArgs extends object> = {
69
71
  };
70
72
 
71
73
  /** Default export metadata describing a component's stories and their shared annotations. */
72
- type ComponentMeta<TArgs extends object> = Annotations<TArgs> & Pick<
73
- ComponentAnnotations<NativeRenderer<TArgs>, TArgs>,
74
- "title" | "id" | "includeStories" | "excludeStories"
75
- > & {
76
- /** Component rendered with story args when no custom render annotation applies. */
77
- component?: ComponentType<TArgs>;
78
- /** Global overrides shared by this component's stories. */
79
- globals?: Args;
80
- };
74
+ type ComponentMeta<TArgs extends object> = Annotations<TArgs> &
75
+ Pick<ComponentAnnotations<NativeRenderer<TArgs>, TArgs>, "title" | "id" | "includeStories" | "excludeStories"> & {
76
+ /** Component rendered with story args when no custom render annotation applies. */
77
+ component?: ComponentType<TArgs>;
78
+ /** Global overrides shared by this component's stories. */
79
+ globals?: Args;
80
+ };
81
81
 
82
82
  /** Resolves a component's props or accepts an explicitly supplied args object type. */
83
- type ComponentOrProps<T> = T extends ComponentType<infer Props extends object>
84
- ? Props
85
- : T extends object ? T : Args;
83
+ type ComponentOrProps<T> = T extends ComponentType<infer Props extends object> ? Props : T extends object ? T : Args;
86
84
 
87
85
  /** Presents combined inferred args as a single object type. */
88
86
  type Simplify<T> = { [Key in keyof T]: T[Key] };
89
87
 
90
- /** Combines the argument requirements of every member of a union. */
91
- type UnionToIntersection<T> = (
92
- T extends unknown ? (value: T) => void : never
93
- ) extends (value: infer Intersection) => void ? Intersection : never;
94
-
95
- /** Retains explicitly named args while removing broad index signatures. */
96
- type DeclaredArgs<T> = {
97
- [Key in keyof T as string extends Key
98
- ? never
99
- : number extends Key ? never : symbol extends Key ? never : Key]: T[Key];
100
- };
101
-
102
- /** Extracts the args accepted by a custom render function. */
103
- type RenderArgs<T> = T extends (args: infer Props extends object, context: never) => ReactNode ? Props : object;
104
-
105
- /** Combines the args required by a metadata object's decorators. */
106
- type DecoratorArgs<T> = T extends readonly (infer Item)[]
107
- ? UnionToIntersection<Item extends Decorator<infer Props extends object> ? Props : object>
108
- : object;
109
-
110
88
  /** Infers story args from component props, render functions, decorators, or declared defaults. */
111
- type ArgsFrom<T> = T extends { component?: infer Component; render?: infer Render; decorators?: infer Decorators }
89
+ type ArgsFrom<T> = T extends { component?: infer Component; render?: unknown; decorators?: unknown }
112
90
  ? Simplify<
113
- (NonNullable<Component> extends ComponentType<infer Props extends object> ? Props : object) &
114
- DeclaredArgs<RenderArgs<NonNullable<Render>> & DecoratorArgs<NonNullable<Decorators>>>
115
- >
116
- : T extends { args: infer Defaults extends object } ? Defaults : Args;
91
+ (NonNullable<Component> extends ComponentType<infer Props extends object> ? Props : object) &
92
+ ArgsFromMeta<NativeRenderer, T>
93
+ >
94
+ : T extends { args: infer Defaults extends object }
95
+ ? Defaults
96
+ : Args;
117
97
 
118
98
  /** CSF3 default export metadata typed from a React component or an explicit args type. */
119
99
  type Meta<T = Args> = ComponentMeta<ComponentOrProps<T>>;
@@ -130,14 +110,16 @@ type StoryAnnotations<TArgs extends object> = Annotations<TArgs> & {
130
110
  type DefaultArgs<T> = T extends { args: infer Defaults } ? Defaults : Record<never, never>;
131
111
 
132
112
  /** Makes args with metadata defaults optional while preserving other required args. */
133
- type RequiredStoryArgs<T, TArgs extends object> =
134
- Omit<TArgs, keyof DefaultArgs<T>> & Partial<Pick<TArgs, keyof TArgs & keyof DefaultArgs<T>>>;
113
+ type RequiredStoryArgs<T, TArgs extends object> = Omit<TArgs, keyof DefaultArgs<T>> &
114
+ Partial<Pick<TArgs, keyof TArgs & keyof DefaultArgs<T>>>;
135
115
 
136
116
  /** CSF3 story object inferred from metadata, a React component, or an explicit args type. */
137
117
  type StoryObj<T = Args> = T extends { component?: unknown; render?: unknown; decorators?: unknown; args?: unknown }
138
- ? StoryAnnotations<ArgsFrom<T>> & (Record<never, never> extends RequiredStoryArgs<T, ArgsFrom<T>>
139
- ? { args?: RequiredStoryArgs<T, ArgsFrom<T>> }
140
- : { args: RequiredStoryArgs<T, ArgsFrom<T>> })
118
+ ? StoryAnnotations<ArgsFrom<T>> &
119
+ Pick<
120
+ StorybookAnnotations<NativeRenderer<ArgsFrom<T>>, ArgsFrom<T>, RequiredStoryArgs<T, ArgsFrom<T>>>,
121
+ "args"
122
+ >
141
123
  : StoryAnnotations<ComponentOrProps<T>>;
142
124
 
143
125
  /** Project annotations shared by all composed stories. */
@@ -177,9 +159,10 @@ type StoryKeys<TModule extends StoryModule> = {
177
159
  [Key in keyof TModule]: Key extends "default" | "__esModule" | "__namedExportsOrder"
178
160
  ? never
179
161
  : TModule[Key] extends StoryAnnotations<ArgsFrom<TModule["default"]>>
180
- ? Key
181
- : never;
182
- }[keyof TModule] & string;
162
+ ? Key
163
+ : never;
164
+ }[keyof TModule] &
165
+ string;
183
166
 
184
167
  /** Composed stories keyed by their original named exports. */
185
168
  type ComposedStories<TModule extends StoryModule> = Record<
package/src/upstream.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { composeStory } from "storybook/preview-api";
2
- import type {
3
- ComponentMeta,
4
- ComposedStory,
5
- Preview,
6
- StoryAnnotations,
7
- StoryRender,
8
- } from "./types.js";
2
+ import type { ComponentMeta, ComposedStory, Preview, StoryAnnotations, StoryRender } from "./types.js";
9
3
 
10
4
  type ComposeStory = <TArgs extends object>(
11
5
  ...annotations: [