@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.
- package/README.md +5 -5
- package/dist/actions-panel.d.ts.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +2 -1
- package/dist/actions.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +11 -5
- package/dist/catalog.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js.map +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/controls.d.ts +2 -1
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +30 -12
- package/dist/controls.js.map +1 -1
- package/dist/explorer-navigator.d.ts.map +1 -1
- package/dist/explorer-navigator.js +29 -2
- package/dist/explorer-navigator.js.map +1 -1
- package/dist/explorer-preview.d.ts.map +1 -1
- package/dist/explorer-preview.js +29 -13
- package/dist/explorer-preview.js.map +1 -1
- package/dist/explorer-view.d.ts +5 -1
- package/dist/explorer-view.d.ts.map +1 -1
- package/dist/explorer-view.js +17 -9
- package/dist/explorer-view.js.map +1 -1
- package/dist/explorer.d.ts +4 -0
- package/dist/explorer.d.ts.map +1 -1
- package/dist/explorer.js +6 -1
- package/dist/explorer.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/upstream.d.ts +4 -2
- package/dist/upstream.d.ts.map +1 -1
- package/dist/upstream.js +3 -1
- package/dist/upstream.js.map +1 -1
- package/package.json +14 -21
- package/src/actions.ts +28 -33
- package/src/catalog.ts +29 -19
- package/src/compose.ts +13 -7
- package/src/config.ts +4 -1
- package/src/controls.tsx +42 -18
- package/src/explorer-navigator.tsx +63 -28
- package/src/explorer-preview.tsx +95 -54
- package/src/explorer-view.tsx +56 -27
- package/src/explorer.ts +12 -1
- package/src/types.ts +42 -30
- 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.
|
|
4
|
-
"description": "
|
|
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.
|
|
68
|
-
"@gtkx/react": "2.0.0-beta.
|
|
69
|
-
"
|
|
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.
|
|
77
|
-
"@gtkx/testing": "2.0.0-beta.
|
|
78
|
-
"@gtkx/vitest": "2.0.0-beta.
|
|
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": "
|
|
73
|
+
"vitest": "5.1.0"
|
|
81
74
|
},
|
|
82
75
|
"scripts": {
|
|
83
|
-
"release": "
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
if (options.onError === undefined) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
options.onError(error);
|
|
152
|
+
};
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
try {
|
|
155
|
+
const result = actionScope.run(store, () => options.callback?.(...values));
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
if (result instanceof Promise) {
|
|
158
|
+
return settleCallback(result, succeed, fail);
|
|
159
|
+
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
succeed();
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
return result;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
fail(error);
|
|
166
166
|
|
|
167
|
-
|
|
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 =
|
|
77
|
-
|
|
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)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 =
|
|
177
|
-
previous
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
+
return createElement(meta.component, args);
|
|
52
|
+
},
|
|
48
53
|
},
|
|
49
|
-
|
|
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
|
-
/**
|
|
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 = (
|
|
33
|
-
const [coefficient = "", exponent = "0"] = String(
|
|
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" ||
|
|
58
|
-
|
|
59
|
-
step
|
|
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 (
|
|
96
|
-
|
|
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 = [
|
|
104
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =>
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 =
|
|
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
|
-
{
|
|
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.
|
|
140
|
+
{groups.length === 0 && (
|
|
106
141
|
<AdwStatusPage
|
|
107
142
|
name="storybook-no-matches"
|
|
108
143
|
title={query ? "No matching stories" : "No stories"}
|