@gtkx/storybook 2.0.0-beta.11 → 2.0.0-beta.13

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.
Files changed (51) hide show
  1. package/README.md +5 -5
  2. package/dist/actions-panel.d.ts.map +1 -1
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +2 -1
  5. package/dist/actions.js.map +1 -1
  6. package/dist/catalog.d.ts +1 -1
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +11 -5
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/compose.d.ts.map +1 -1
  11. package/dist/compose.js.map +1 -1
  12. package/dist/config.d.ts +4 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js.map +1 -1
  15. package/dist/controls.d.ts +2 -1
  16. package/dist/controls.d.ts.map +1 -1
  17. package/dist/controls.js +30 -12
  18. package/dist/controls.js.map +1 -1
  19. package/dist/explorer-navigator.d.ts.map +1 -1
  20. package/dist/explorer-navigator.js +29 -2
  21. package/dist/explorer-navigator.js.map +1 -1
  22. package/dist/explorer-preview.d.ts.map +1 -1
  23. package/dist/explorer-preview.js +29 -13
  24. package/dist/explorer-preview.js.map +1 -1
  25. package/dist/explorer-view.d.ts +5 -1
  26. package/dist/explorer-view.d.ts.map +1 -1
  27. package/dist/explorer-view.js +17 -9
  28. package/dist/explorer-view.js.map +1 -1
  29. package/dist/explorer.d.ts +4 -0
  30. package/dist/explorer.d.ts.map +1 -1
  31. package/dist/explorer.js +6 -1
  32. package/dist/explorer.js.map +1 -1
  33. package/dist/types.d.ts +8 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/upstream.d.ts +4 -2
  37. package/dist/upstream.d.ts.map +1 -1
  38. package/dist/upstream.js +3 -1
  39. package/dist/upstream.js.map +1 -1
  40. package/package.json +14 -21
  41. package/src/actions.ts +28 -33
  42. package/src/catalog.ts +29 -19
  43. package/src/compose.ts +13 -7
  44. package/src/config.ts +4 -1
  45. package/src/controls.tsx +42 -18
  46. package/src/explorer-navigator.tsx +63 -28
  47. package/src/explorer-preview.tsx +95 -54
  48. package/src/explorer-view.tsx +56 -27
  49. package/src/explorer.ts +12 -1
  50. package/src/types.ts +42 -30
  51. package/src/upstream.ts +6 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gtkx/storybook",
3
- "version": "2.0.0-beta.11",
4
- "description": "Explore GTKX components as native stories.",
3
+ "version": "2.0.0-beta.13",
4
+ "description": "Native component stories for GTKX.",
5
5
  "keywords": [
6
6
  "gtkx",
7
7
  "gtk",
@@ -10,7 +10,9 @@
10
10
  "react",
11
11
  "storybook",
12
12
  "csf",
13
- "linux"
13
+ "linux",
14
+ "gnome",
15
+ "desktop"
14
16
  ],
15
17
  "homepage": "https://gtkx.dev",
16
18
  "bugs": {
@@ -50,36 +52,27 @@
50
52
  "dist",
51
53
  "src"
52
54
  ],
53
- "nx": {
54
- "targets": {
55
- "lint": {
56
- "options": {
57
- "cwd": ".",
58
- "command": "eslint {projectRoot}"
59
- }
60
- }
61
- }
62
- },
63
55
  "engines": {
64
56
  "node": ">=26.7.0"
65
57
  },
66
58
  "dependencies": {
67
- "@gtkx/components": "2.0.0-beta.11",
68
- "@gtkx/react": "2.0.0-beta.11",
69
- "storybook": "10.6.0"
59
+ "@gtkx/components": "^2.0.0-beta.13",
60
+ "@gtkx/react": "^2.0.0-beta.13",
61
+ "@gtkx/utils": "^2.0.0-beta.13",
62
+ "storybook": "10.6.5"
70
63
  },
71
64
  "peerDependencies": {
72
65
  "@types/react": "^19.3",
73
66
  "react": "^19.3"
74
67
  },
75
68
  "devDependencies": {
76
- "@gtkx/config": "2.0.0-beta.11",
77
- "@gtkx/testing": "2.0.0-beta.11",
78
- "@gtkx/vitest": "2.0.0-beta.11",
69
+ "@gtkx/config": "2.0.0-beta.13",
70
+ "@gtkx/testing": "2.0.0-beta.13",
71
+ "@gtkx/vitest": "2.0.0-beta.13",
79
72
  "@types/react": "^19.3.0",
80
- "vitest": "^5.0.1"
73
+ "vitest": "5.1.0"
81
74
  },
82
75
  "scripts": {
83
- "release": "tsx ../../scripts/release-package.ts"
76
+ "release": "node ../../scripts/release/publish-package.ts"
84
77
  }
85
78
  }
package/src/actions.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { normalizeError } from "@gtkx/utils";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { inspect } from "node:util";
3
4
  import type { Args, ArgTypes } from "./types.js";
@@ -133,40 +134,39 @@ const settleCallback = async (
133
134
  }
134
135
  };
135
136
 
136
- const bindCallback = (
137
- store: ActionStore,
138
- options: CallbackOptions,
139
- ): EventCallback => (...values) => {
140
- const succeed = (): void => {
141
- store.record(options.name, values);
142
- };
143
- const fail = (cause: unknown): void => {
144
- const error = cause instanceof Error ? cause : new Error(String(cause));
145
- store.record(options.name, values, error);
137
+ const bindCallback =
138
+ (store: ActionStore, options: CallbackOptions): EventCallback =>
139
+ (...values) => {
140
+ const succeed = (): void => {
141
+ store.record(options.name, values);
142
+ };
143
+ const fail = (cause: unknown): void => {
144
+ const error = normalizeError(cause);
145
+ store.record(options.name, values, error);
146
146
 
147
- if (options.onError === undefined) {
148
- throw error;
149
- }
147
+ if (options.onError === undefined) {
148
+ throw error;
149
+ }
150
150
 
151
- options.onError(error);
152
- };
151
+ options.onError(error);
152
+ };
153
153
 
154
- try {
155
- const result = actionScope.run(store, () => options.callback?.(...values));
154
+ try {
155
+ const result = actionScope.run(store, () => options.callback?.(...values));
156
156
 
157
- if (result instanceof Promise) {
158
- return settleCallback(result, succeed, fail);
159
- }
157
+ if (result instanceof Promise) {
158
+ return settleCallback(result, succeed, fail);
159
+ }
160
160
 
161
- succeed();
161
+ succeed();
162
162
 
163
- return result;
164
- } catch (error) {
165
- fail(error);
163
+ return result;
164
+ } catch (error) {
165
+ fail(error);
166
166
 
167
- return;
168
- }
169
- };
167
+ return;
168
+ }
169
+ };
170
170
 
171
171
  const actionName = (value: unknown, configuredName: unknown): string | undefined => {
172
172
  if (typeof configuredName === "string") {
@@ -195,12 +195,7 @@ const callbackOptions = (
195
195
  };
196
196
  };
197
197
 
198
- const bindActions = (
199
- args: Args,
200
- argTypes: ArgTypes,
201
- store: ActionStore,
202
- onError?: (error: Error) => void,
203
- ): Args => {
198
+ const bindActions = (args: Args, argTypes: ArgTypes, store: ActionStore, onError?: (error: Error) => void): Args => {
204
199
  const bound = { ...args };
205
200
  const names = new Set([...Object.keys(args), ...Object.keys(argTypes)]);
206
201
 
package/src/catalog.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { normalizeError } from "@gtkx/utils";
1
2
  import type { Args, ArgTypes, ComponentMeta, ComposedStory, Preview, StoryAnnotations } from "./types.js";
2
3
  import { composeStories } from "./compose.js";
3
4
  import { storyNameFromExport } from "./upstream.js";
@@ -40,7 +41,7 @@ type StoryLoadError = {
40
41
 
41
42
  /** Current explorer contents and loading state exposed to subscribers. */
42
43
  type StoryCatalogSnapshot = {
43
- /** Successfully composed stories with duplicate identifiers excluded. */
44
+ /** Successfully composed stories tagged `dev`, with duplicate identifiers excluded. */
44
45
  stories: StoryEntry[];
45
46
  /** Failures encountered during the latest load or reported by the session. */
46
47
  errors: StoryLoadError[];
@@ -58,8 +59,7 @@ type CachedStories = {
58
59
  type LoadedSource = StoryLoadError | { source: StorySource; cached: CachedStories };
59
60
 
60
61
  const EMPTY_PREVIEW: Preview = {};
61
-
62
- const normalizeError = (cause: unknown): Error => cause instanceof Error ? cause : new Error(String(cause));
62
+ const DEVELOPMENT_TAG = "dev";
63
63
 
64
64
  const isObject = (value: unknown): value is Record<string, unknown> =>
65
65
  value !== null && typeof value === "object" && !Array.isArray(value);
@@ -73,8 +73,10 @@ const explicitControls = (
73
73
  const controls: ArgTypes = {};
74
74
 
75
75
  for (const [argument, annotation] of Object.entries(resolved)) {
76
- const control = story?.argTypes?.[argument]?.control ??
77
- meta.argTypes?.[argument]?.control ?? preview.argTypes?.[argument]?.control;
76
+ const control =
77
+ story?.argTypes?.[argument]?.control ??
78
+ meta.argTypes?.[argument]?.control ??
79
+ preview.argTypes?.[argument]?.control;
78
80
 
79
81
  if (control === undefined) {
80
82
  continue;
@@ -117,15 +119,17 @@ const composeSource = (source: StorySource, module: unknown, preview: Preview):
117
119
  } as Record<string, StoryAnnotations<Args>> & { default: ComponentMeta<Args> };
118
120
  const stories = composeStories(annotations, preview);
119
121
 
120
- return Object.entries(stories).map(([exportName, story]) => ({
121
- id: story.id,
122
- title,
123
- name: displayName(annotations[exportName], exportName),
124
- exportName,
125
- source: source.id,
126
- story,
127
- controls: explicitControls(story.argTypes, annotations[exportName], annotations.default, preview),
128
- }));
122
+ return Object.entries(stories)
123
+ .filter(([, story]) => story.tags.includes(DEVELOPMENT_TAG))
124
+ .map(([exportName, story]) => ({
125
+ id: story.id,
126
+ title,
127
+ name: displayName(annotations[exportName], exportName),
128
+ exportName,
129
+ source: source.id,
130
+ story,
131
+ controls: explicitControls(story.argTypes, annotations[exportName], annotations.default, preview),
132
+ }));
129
133
  };
130
134
 
131
135
  const withoutDuplicateIds = (entries: StoryEntry[], errors: StoryLoadError[]): StoryEntry[] => {
@@ -173,10 +177,13 @@ const loadSource = async (
173
177
  try {
174
178
  const module = await source.load();
175
179
  const previous = getCache().get(source.id);
176
- const cached = previous !== undefined && previous.module === module &&
177
- previous.title === source.title && previous.preview === preview
178
- ? previous
179
- : { module, title: source.title, preview, entries: composeSource(source, module, preview) };
180
+ const cached =
181
+ previous !== undefined &&
182
+ previous.module === module &&
183
+ previous.title === source.title &&
184
+ previous.preview === preview
185
+ ? previous
186
+ : { module, title: source.title, preview, entries: composeSource(source, module, preview) };
180
187
 
181
188
  return { source, cached };
182
189
  } catch (error) {
@@ -233,7 +240,10 @@ class StoryCatalog {
233
240
  this.publish({ stories, errors, isLoading: false });
234
241
 
235
242
  if (errors.length > 0) {
236
- const failure = new AggregateError(errors.map(({ error }) => error), "Some stories could not be loaded");
243
+ const failure = new AggregateError(
244
+ errors.map(({ error }) => error),
245
+ "Some stories could not be loaded",
246
+ );
237
247
  this.loadFailures.add(failure);
238
248
  throw failure;
239
249
  }
package/src/compose.ts CHANGED
@@ -38,15 +38,21 @@ const composeStory = <TArgs extends object>(
38
38
  throw new TypeError("A story needs a component or render function");
39
39
  }
40
40
 
41
- return composeStoryAnnotations(story, { ...meta }, preview, {
42
- render: (args) => {
43
- if (!meta.component) {
44
- throw new TypeError("A story needs a component or render function");
45
- }
41
+ return composeStoryAnnotations(
42
+ story,
43
+ { ...meta },
44
+ preview,
45
+ {
46
+ render: (args) => {
47
+ if (!meta.component) {
48
+ throw new TypeError("A story needs a component or render function");
49
+ }
46
50
 
47
- return createElement(meta.component, args);
51
+ return createElement(meta.component, args);
52
+ },
48
53
  },
49
- }, exportName);
54
+ exportName,
55
+ );
50
56
  };
51
57
 
52
58
  /**
package/src/config.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /** Story discovery and preview configuration for the native explorer. */
2
2
  type StorybookConfig = {
3
- /** Project-relative globs; defaults to CSF story files under src. */
3
+ /**
4
+ * Project-relative globs; defaults to CSF story files under src. A glob may reach a sibling workspace
5
+ * package through `..` segments when every file it matches lies inside the workspace root.
6
+ */
4
7
  stories?: string[];
5
8
  /** Additional project-relative globs excluded from discovery. */
6
9
  exclude?: string[];
package/src/controls.tsx CHANGED
@@ -4,8 +4,9 @@ import { markupEscapeText } from "@gtkx/gi/glib";
4
4
  import { AdwActionRow, AdwEntryRow, AdwPreferencesGroup, AdwSpinRow, AdwSwitchRow } from "@gtkx/jsx/adw";
5
5
  import { GtkAdjustment } from "@gtkx/jsx/gtk";
6
6
  import type { Args, ArgTypes } from "./types.js";
7
+ import { isConditionalArgIncluded } from "./upstream.js";
7
8
 
8
- type ControlsProps = { argTypes: ArgTypes; args: Args; onChange: (update: Args) => void };
9
+ type ControlsProps = { argTypes: ArgTypes; args: Args; globals: Args; onChange: (update: Args) => void };
9
10
 
10
11
  const NO_SELECTION_ID = "no-selection";
11
12
  const NO_SELECTION_LABEL = "Choose option...";
@@ -27,10 +28,10 @@ const isObject = (value: unknown): value is Args =>
27
28
  const isWithinBoundsOrAbsent = (value: unknown, min: number, max: number): boolean =>
28
29
  value === undefined || (typeof value === "number" && Number.isFinite(value) && value >= min && value <= max);
29
30
 
30
- const controlTitle = (name: unknown, argument: string): string => typeof name === "string" ? name : argument;
31
+ const controlTitle = (name: unknown, argument: string): string => (typeof name === "string" ? name : argument);
31
32
 
32
- const numberDigits = (step: number): number => {
33
- const [coefficient = "", exponent = "0"] = String(step).split("e", 2);
33
+ const numberDigits = (value: number): number => {
34
+ const [coefficient = "", exponent = "0"] = String(value).split("e", 2);
34
35
  const fraction = coefficient.split(".", 2)[1]?.length ?? 0;
35
36
  const digits = Math.max(0, fraction - Number(exponent));
36
37
 
@@ -54,9 +55,15 @@ const NumberControl = ({
54
55
  const step = settings.step ?? 1;
55
56
 
56
57
  if (
57
- typeof min !== "number" || typeof max !== "number" || typeof step !== "number" ||
58
- !Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(step) ||
59
- step <= 0 || min > max || !isWithinBoundsOrAbsent(value, min, max)
58
+ typeof min !== "number" ||
59
+ typeof max !== "number" ||
60
+ typeof step !== "number" ||
61
+ !Number.isFinite(min) ||
62
+ !Number.isFinite(max) ||
63
+ !Number.isFinite(step) ||
64
+ step <= 0 ||
65
+ min > max ||
66
+ !isWithinBoundsOrAbsent(value, min, max)
60
67
  ) {
61
68
  return <UnsupportedControl title={title} description="Provide a finite number and valid bounds." />;
62
69
  }
@@ -69,10 +76,10 @@ const NumberControl = ({
69
76
  title={markupEscapeText(title, -1)}
70
77
  sensitive={!isDisabled}
71
78
  value={currentValue}
72
- digits={numberDigits(step)}
79
+ digits={Math.max(numberDigits(step), numberDigits(currentValue))}
73
80
  numeric
74
81
  widthChars={8}
75
- adjustment={(
82
+ adjustment={
76
83
  <GtkAdjustment
77
84
  lower={min}
78
85
  upper={max}
@@ -80,7 +87,7 @@ const NumberControl = ({
80
87
  pageIncrement={Math.min(Number.MAX_VALUE, step * 10)}
81
88
  value={currentValue}
82
89
  />
83
- )}
90
+ }
84
91
  onNotifyValue={(next) => {
85
92
  if (!Object.is(next, value)) {
86
93
  onChange({ [argument]: next });
@@ -92,19 +99,31 @@ const NumberControl = ({
92
99
 
93
100
  const SelectControl = (props: Omit<ControlProps, "type">): ReactNode => {
94
101
  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")) {
102
+ if (
103
+ !Array.isArray(options) ||
104
+ options.length === 0 ||
105
+ options.some(
106
+ (option: unknown) =>
107
+ option !== null &&
108
+ typeof option !== "string" &&
109
+ typeof option !== "number" &&
110
+ typeof option !== "boolean",
111
+ )
112
+ ) {
97
113
  return <UnsupportedControl title={title} description="Provide a list of text, number, or boolean options." />;
98
114
  }
99
115
 
100
116
  const values: unknown[] = options;
101
117
  const selected = values.findIndex((option) => Object.is(option, value));
102
118
  const labels = isObject(settings.labels) ? settings.labels : {};
103
- const items = [{ id: NO_SELECTION_ID, value: NO_SELECTION_LABEL }, ...values.map((option, index) => {
104
- const label = labels[String(option)];
119
+ const items = [
120
+ { id: NO_SELECTION_ID, value: NO_SELECTION_LABEL },
121
+ ...values.map((option, index) => {
122
+ const label = labels[String(option)];
105
123
 
106
- return { id: String(index), value: typeof label === "string" ? label : String(option) };
107
- })];
124
+ return { id: String(index), value: typeof label === "string" ? label : String(option) };
125
+ }),
126
+ ];
108
127
 
109
128
  return (
110
129
  <ComboRow
@@ -179,13 +198,18 @@ const Control = (props: ControlProps): ReactNode => {
179
198
  }
180
199
  };
181
200
 
182
- const Controls = ({ argTypes, args, onChange }: ControlsProps): ReactNode => (
201
+ const Controls = ({ argTypes, args, globals, onChange }: ControlsProps): ReactNode => (
183
202
  <AdwPreferencesGroup title="Controls">
184
203
  {Object.entries(argTypes).map(([argument, annotation]) => {
185
204
  const control: unknown = annotation?.control;
186
205
  const table = isObject(annotation?.table) ? annotation.table : {};
187
206
 
188
- if (control === undefined || control === false || table.disable === true) {
207
+ if (
208
+ control === undefined ||
209
+ control === false ||
210
+ table.disable === true ||
211
+ !isConditionalArgIncluded(annotation ?? {}, args, globals)
212
+ ) {
189
213
  return null;
190
214
  }
191
215
 
@@ -18,16 +18,51 @@ type GroupProps = Pick<NavigatorProps, "selectedId" | "onSelect"> & {
18
18
  entries: StoryEntry[];
19
19
  };
20
20
 
21
+ type Group = {
22
+ title: string;
23
+ entries: StoryEntry[];
24
+ path: number[];
25
+ };
26
+
27
+ const TITLE_SEPARATOR = "/";
28
+
29
+ const titlePrefixes = (title: string): string[] =>
30
+ title.split(TITLE_SEPARATOR).map((_, index, segments) => segments.slice(0, index + 1).join(TITLE_SEPARATOR));
31
+
32
+ const compareIndexPaths = (left: number[], right: number[]): number => {
33
+ for (const [index, value] of left.entries()) {
34
+ const other = right[index];
35
+
36
+ if (other === undefined) {
37
+ return 1;
38
+ }
39
+
40
+ if (value !== other) {
41
+ return value - other;
42
+ }
43
+ }
44
+
45
+ return left.length - right.length;
46
+ };
47
+
48
+ /**
49
+ * Groups stories by title, keeping titles that share a path prefix together as Storybook's sidebar nests
50
+ * them: the first title under each prefix decides where the prefix appears.
51
+ */
52
+ const groupStoriesByTitle = (stories: StoryEntry[]): Group[] => {
53
+ const firstOccurrence: Map<string, number> = new Map();
54
+ const groups = [...Map.groupBy(stories, (entry) => entry.title)].map(([title, entries], order) => ({
55
+ title,
56
+ entries,
57
+ path: titlePrefixes(title).map((prefix) => firstOccurrence.getOrInsert(prefix, order)),
58
+ }));
59
+
60
+ return groups.toSorted((left, right) => compareIndexPaths(left.path, right.path));
61
+ };
62
+
21
63
  const StoryGroup = ({ title, entries, selectedId, onSelect }: GroupProps): ReactNode => (
22
64
  <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
- />
65
+ <GtkLabel label={title} xalign={0} marginStart={18} marginEnd={18} cssClasses={["heading"]} wrap />
31
66
  <GtkListBox
32
67
  cssClasses={["navigation-sidebar"]}
33
68
  selectedIndex={entries.findIndex((entry) => entry.id === selectedId)}
@@ -54,29 +89,29 @@ const StoryGroup = ({ title, entries, selectedId, onSelect }: GroupProps): React
54
89
  </GtkBox>
55
90
  );
56
91
 
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
- );
92
+ const LoadErrors = ({ errors }: Pick<NavigatorProps, "errors">): ReactNode =>
93
+ errors.length > 0 && (
94
+ <GtkBox name="storybook-load-errors" orientation={Gtk.Orientation.VERTICAL} spacing={6}>
95
+ {errors.map(({ source, error }, index) => (
96
+ <GtkLabel
97
+ key={`${source}-${String(index)}`}
98
+ label={`${source}\n${error.message.slice(0, 500)}`}
99
+ cssClasses={["error"]}
100
+ marginStart={18}
101
+ marginEnd={18}
102
+ wrap
103
+ selectable
104
+ xalign={0}
105
+ />
106
+ ))}
107
+ </GtkBox>
108
+ );
73
109
 
74
110
  const StoryNavigator = ({ stories, errors, selectedId, onSelect }: NavigatorProps): ReactNode => {
75
111
  const [search, setSearch] = useState("");
76
112
  const query = search.trim().toLocaleLowerCase();
77
- const groups = Map.groupBy(
113
+ const groups = groupStoriesByTitle(
78
114
  stories.filter((entry) => `${entry.title} ${entry.name}`.toLocaleLowerCase().includes(query)),
79
- (entry) => entry.title,
80
115
  );
81
116
 
82
117
  return (
@@ -93,7 +128,7 @@ const StoryNavigator = ({ stories, errors, selectedId, onSelect }: NavigatorProp
93
128
  />
94
129
  <GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER} vexpand>
95
130
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={18} marginBottom={18}>
96
- {[...groups].map(([title, entries]) => (
131
+ {groups.map(({ title, entries }) => (
97
132
  <StoryGroup
98
133
  key={title}
99
134
  title={title}
@@ -102,7 +137,7 @@ const StoryNavigator = ({ stories, errors, selectedId, onSelect }: NavigatorProp
102
137
  onSelect={onSelect}
103
138
  />
104
139
  ))}
105
- {groups.size === 0 && (
140
+ {groups.length === 0 && (
106
141
  <AdwStatusPage
107
142
  name="storybook-no-matches"
108
143
  title={query ? "No matching stories" : "No stories"}