forgebook 0.3.1 → 0.3.3
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 +15 -5
- package/dist/bin/forgebook/agents.md +5 -4
- package/dist/src/addressArgs.d.ts +25 -1
- package/dist/src/addressArgs.js +15 -1
- package/dist/src/shell/EmbedPage.js +33 -7
- package/dist/src/shell/PluginBlockHost.js +2 -1
- package/dist/src/shell/PluginTabHost.js +19 -2
- package/dist/src/shell/SearchPanel.js +2 -3
- package/dist/src/shell/Shell.js +14 -5
- package/dist/src/shell/Sidebar.d.ts +8 -2
- package/dist/src/shell/Sidebar.js +3 -3
- package/dist/src/shell/SidebarHandle.d.ts +14 -1
- package/dist/src/shell/SidebarHandle.js +10 -0
- package/dist/src/shell/SidebarSearch.js +7 -4
- package/dist/src/shell/Stage.js +7 -12
- package/dist/src/shell/Toolbar.d.ts +6 -4
- package/dist/src/shell/Toolbar.js +3 -3
- package/dist/src/shell/asideStory.d.ts +17 -4
- package/dist/src/shell/asideStory.js +14 -2
- package/dist/src/shell/shell.css +28 -1
- package/dist/src/shell/useAddressArgs.d.ts +18 -2
- package/dist/src/shell/useAddressArgs.js +19 -7
- package/dist/src/shell/useSidebarLayout.d.ts +13 -3
- package/dist/src/shell/useSidebarLayout.js +43 -5
- package/dist/src/shell/useSidebarOverlay.d.ts +34 -0
- package/dist/src/shell/useSidebarOverlay.js +51 -0
- package/dist/src/storyMocks.d.ts +53 -0
- package/dist/src/storyMocks.js +208 -0
- package/dist/src/vite/docsExtractor.d.ts +8 -1
- package/dist/src/vite/docsExtractor.js +52 -10
- package/dist/src/vite/docsWorker.js +6 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ per story, switches for theme, contrast, motion and language, and a props table
|
|
|
9
9
|
> and every change is in the
|
|
10
10
|
> [changelog](https://github.com/ensiria/forgebook/blob/develop/packages/forgebook/CHANGELOG.md).
|
|
11
11
|
|
|
12
|
-
**Requirements:** Node 22.18 or later · Vite 8 (the catalogue is built on it; other bundlers are not supported) · React 19 through
|
|
12
|
+
**Requirements:** Node 22.18 or later · Vite 8.3.4+ (the catalogue is built on it; other bundlers are not supported) · React 19 through
|
|
13
13
|
[`@forgebook/react`](https://www.npmjs.com/package/@forgebook/react), Vue 3.5 through
|
|
14
14
|
[`@forgebook/vue`](https://www.npmjs.com/package/@forgebook/vue) (a Nuxt project's through
|
|
15
15
|
[`@forgebook/nuxt`](https://www.npmjs.com/package/@forgebook/nuxt)), Svelte 5 through
|
|
@@ -194,6 +194,15 @@ too, with a warning.
|
|
|
194
194
|
|
|
195
195
|
It adds about a dozen packages to a React and Vite project.
|
|
196
196
|
|
|
197
|
+
## On a narrow window
|
|
198
|
+
|
|
199
|
+
Below 720 px the sidebar stops taking room from the page. It starts closed, opens from the toolbar's sidebar button and
|
|
200
|
+
slides over the page, which keeps the whole width and cannot be reached while the sidebar is open. A tap outside it,
|
|
201
|
+
Escape (once the search is empty and no menu is open), its own button or opening any page closes it, and the focus goes
|
|
202
|
+
back to the button, unless a search's result has taken it to what it found. Opening it there changes
|
|
203
|
+
nothing a wider window remembers: the sidebar a person showed or hid on a wide window is as they left it, width
|
|
204
|
+
included. A reduced-motion preference shows and hides it at once.
|
|
205
|
+
|
|
197
206
|
## The toolbar
|
|
198
207
|
|
|
199
208
|
The View menu holds every control: the catalogue's switches, and Forgebook's own (the background, the style sheets, the
|
|
@@ -498,8 +507,8 @@ The buttons read their colours from the page's theme.
|
|
|
498
507
|
|
|
499
508
|
The field at the top of the sidebar searches the whole catalogue. The tree filters by name as you type, for browsing;
|
|
500
509
|
beside it a list ranks everything the words find, each component or page with what matched inside it and why (a prop,
|
|
501
|
-
an event, a heading, a sentence, a story), the words marked. `/` or ⌘K (Ctrl+K) comes to the field from anywhere,
|
|
502
|
-
move, Enter opens at the exact heading or prop, ⌘Enter or a modified press opens a new tab, Escape empties the field.
|
|
510
|
+
an event, a heading, a sentence, a story), the words marked. `/` or ⌘K (Ctrl+K) comes to the field from anywhere (with the sidebar hidden, ⌘K alone,
|
|
511
|
+
which shows it), ↑ and ↓ move, Enter opens at the exact heading or prop, ⌘Enter or a modified press opens a new tab, Escape empties the field.
|
|
503
512
|
|
|
504
513
|
A prop has an address of its own, so a link to one can be pasted anywhere: `?component=acme:inputs-select#prop.value`,
|
|
505
514
|
and `#prop.Group.label` for a prop of a part named in `subcomponents`. A heading of a component's documentation file
|
|
@@ -1086,8 +1095,9 @@ it, and the Embed button's **Follow the story's height** adds the few lines that
|
|
|
1086
1095
|
iframe, since its modules are loaded as those of its own origin.
|
|
1087
1096
|
|
|
1088
1097
|
An embed sends nothing but the height and listens to nothing from the page around it, and it keeps working where a browser
|
|
1089
|
-
denies a third-party frame its storage. What it does take from outside is its address
|
|
1090
|
-
|
|
1098
|
+
denies a third-party frame its storage. What it does take from outside is its address, checked as the catalogue's own
|
|
1099
|
+
`args` are: a story receives only the props its panel can set, with a value of their type. A prop the story does declare
|
|
1100
|
+
still reaches it, so a story that renders a prop as raw HTML should not be published for anyone to frame.
|
|
1091
1101
|
And a height says something: a page that can reach your catalogue can tell whether a story exists.
|
|
1092
1102
|
|
|
1093
1103
|
Forgebook sets no framing headers, on the dev server or in a build, so what you host keeps the rules it has. To choose who may
|
|
@@ -84,9 +84,9 @@ in JSON with `--json`, and every answer about a story carries the links that ope
|
|
|
84
84
|
the window (`style={{ flex: 1 }}`) needs `parameters.layout: "fullscreen"`; the app's root providers
|
|
85
85
|
(`SafeAreaProvider`, a theme) go in the preview's decorators. A component that calls a native module or a gesture
|
|
86
86
|
of native code does not draw in a browser: give it a `.web.tsx` file, or mock what it calls in the preview.
|
|
87
|
-
- A link may carry the Props tab's values (`args=` in the address, JSON). The catalogue applies only the
|
|
88
|
-
story's panel can set, with a value of their type; to show a story with other values, write a story with
|
|
89
|
-
args rather than a link.
|
|
87
|
+
- A link or an embed may carry the Props tab's values (`args=` in the address, JSON). The catalogue applies only the
|
|
88
|
+
props the story's panel can set, with a value of their type; to show a story with other values, write a story with
|
|
89
|
+
those args rather than a link.
|
|
90
90
|
- `parameters.height` gives the frame the room an overlay the story opens needs (a menu, a popover), in CSS px.
|
|
91
91
|
- A screen meant for a phone or a tablet can be shown in a drawn device by default:
|
|
92
92
|
`parameters: { device: { frame: true, model: "iphone-16" } }` (on the meta for every story; `orientation:
|
|
@@ -115,7 +115,8 @@ in JSON with `--json`, and every answer about a story carries the links that ope
|
|
|
115
115
|
and what its server code reads with `nextjs.cookies`, `headers` and `draftMode`. A link or a router call moves the
|
|
116
116
|
story's route and is listed among its events; a `play` reads where it went with `storyRoute()`
|
|
117
117
|
(`@forgebook/nextjs/route`). Async server components are drawn; what they reach on a server (a database, the file
|
|
118
|
-
system) is not there: fetch it and mock it with `parameters.msw`.
|
|
118
|
+
system) is not there: fetch it and mock it with `parameters.msw`. In the server tests a server component's `fetch` never
|
|
119
|
+
reaches the network, whatever its address: a fetch no handler answers fails the story, naming the address.
|
|
119
120
|
|
|
120
121
|
- Say when a component is for and when it is not, in its TSDoc: `@useWhen`, `@avoidWhen`, `@alternatives`, `@deprecated`,
|
|
121
122
|
`@replacedBy`. `forgebook story` returns them as `docs.guidance`: read them before using a component, and never use a
|
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
* left out and reported. A value that is code never comes from an address: a named value is looked up among the
|
|
13
13
|
* source's own entries only, never through an object's prototype (`constructor`). Pure.
|
|
14
14
|
*
|
|
15
|
+
* A frame takes values from one kind of message only, whichever frame it is (the stage's, an embed's, a plugin's aside),
|
|
16
|
+
* so `checkedArgsMessage` builds it, from nothing but what this check sealed (`AcceptedArgs`): a page that skipped the
|
|
17
|
+
* check has no value of that type to give it. A test holds every other module to not writing the message itself.
|
|
18
|
+
*
|
|
15
19
|
* @module
|
|
16
20
|
*/
|
|
17
21
|
import type { StoryControlsSetting } from "@forgebook/story-format/source";
|
|
18
22
|
import { type PanelArgTypes, type PanelProp } from "./argTypes.ts";
|
|
19
23
|
import type { PropDocs } from "./componentDocs.ts";
|
|
20
|
-
import { type ArgValue, type PanelArgs } from "./messages.ts";
|
|
24
|
+
import { type ArgValue, type PanelArgs, type ShellMessage } from "./messages.ts";
|
|
21
25
|
import { type NamedValues } from "./namedValues.ts";
|
|
22
26
|
/** What a story declares about the args the panel sets, as the shell knows it once the story's frame is ready. */
|
|
23
27
|
export type ArgDeclaration = {
|
|
@@ -47,7 +51,20 @@ export type CheckedArgs = {
|
|
|
47
51
|
readonly accepted: PanelArgs;
|
|
48
52
|
/** The values left out, in the address's order. */
|
|
49
53
|
readonly dropped: readonly DroppedArg[];
|
|
54
|
+
/** The values the story receives, as a frame can be told them. */
|
|
55
|
+
readonly sealed: AcceptedArgs;
|
|
56
|
+
};
|
|
57
|
+
/** What only this module can write on values: the type system's proof that they went through the check. */
|
|
58
|
+
declare const ACCEPTED: unique symbol;
|
|
59
|
+
/** Values the check accepted, the only ones a frame can be told. */
|
|
60
|
+
export type AcceptedArgs = {
|
|
61
|
+
/** The values. */
|
|
62
|
+
readonly values: PanelArgs;
|
|
63
|
+
/** Marks them as the check's. */
|
|
64
|
+
readonly [ACCEPTED]: true;
|
|
50
65
|
};
|
|
66
|
+
/** No values: what a frame the address has no say in receives (a story on a docs page). */
|
|
67
|
+
export declare const NO_ACCEPTED_ARGS: AcceptedArgs;
|
|
51
68
|
/**
|
|
52
69
|
* The props the panel sets for a story, whatever the address says: the component's props with the `argTypes` over
|
|
53
70
|
* them and the args only the `argTypes` name, then the story's own args the two leave out, each kept only when the
|
|
@@ -72,3 +89,10 @@ export declare const isValueOfProp: (prop: PanelProp, value: ArgValue, namedValu
|
|
|
72
89
|
* @param declaration - what the story declares.
|
|
73
90
|
*/
|
|
74
91
|
export declare const checkedAddressArgs: (args: PanelArgs, declaration: ArgDeclaration) => CheckedArgs;
|
|
92
|
+
/**
|
|
93
|
+
* The message that gives a frame its values.
|
|
94
|
+
*
|
|
95
|
+
* @param accepted - the values `checkedAddressArgs` accepted.
|
|
96
|
+
*/
|
|
97
|
+
export declare const checkedArgsMessage: (accepted: AcceptedArgs) => ShellMessage;
|
|
98
|
+
export {};
|
package/dist/src/addressArgs.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { panelProps, settableProps } from "./argTypes.js";
|
|
2
2
|
import { isChoiceList, isNamedArg } from "./messages.js";
|
|
3
3
|
import { namedValueOf } from "./namedValues.js";
|
|
4
|
+
/** What only this module can write on values: the type system's proof that they went through the check. */
|
|
5
|
+
const ACCEPTED = Symbol("accepted");
|
|
6
|
+
/** No values: what a frame the address has no say in receives (a story on a docs page). */
|
|
7
|
+
export const NO_ACCEPTED_ARGS = { values: {}, [ACCEPTED]: true };
|
|
4
8
|
/** A prop made from one of the story's own args, for an arg neither the types nor the `argTypes` name. */
|
|
5
9
|
const propOfStoryArg = (name, value) => {
|
|
6
10
|
const prop = { name, type: "", description: "", isRequired: false };
|
|
@@ -105,5 +109,15 @@ export const checkedAddressArgs = (args, declaration) => {
|
|
|
105
109
|
else
|
|
106
110
|
accepted.push([name, value]);
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const values = Object.fromEntries(accepted);
|
|
113
|
+
return { accepted: values, dropped, sealed: { values, [ACCEPTED]: true } };
|
|
109
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* The message that gives a frame its values.
|
|
117
|
+
*
|
|
118
|
+
* @param accepted - the values `checkedAddressArgs` accepted.
|
|
119
|
+
*/
|
|
120
|
+
export const checkedArgsMessage = (accepted) => ({
|
|
121
|
+
type: "forgebook:args",
|
|
122
|
+
args: accepted.values
|
|
123
|
+
});
|
|
@@ -15,9 +15,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
*/
|
|
16
16
|
import "./embed.css";
|
|
17
17
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
18
|
+
import { checkedArgsMessage } from "../addressArgs.js";
|
|
18
19
|
import { addressArgsFor } from "../addressTexts.js";
|
|
19
20
|
import { EMBED_SIZE_TYPE } from "../embed.js";
|
|
20
21
|
import { parsePanelArgsParam } from "../messages.js";
|
|
22
|
+
import { sourceFiles } from "../sourceFiles.js";
|
|
21
23
|
import { storyBackground } from "../storyBackground.js";
|
|
22
24
|
import { NOT_FIXED, withFixed } from "../storyGlobals.js";
|
|
23
25
|
import { findStory, storyTree } from "../storyTree.js";
|
|
@@ -28,11 +30,17 @@ import { framesOrigin } from "./framesOrigin.js";
|
|
|
28
30
|
import { LATER_STRINGS } from "./laterStrings.js";
|
|
29
31
|
import { shellIsDark } from "./shellScheme.js";
|
|
30
32
|
import { STRINGS } from "./strings.js";
|
|
33
|
+
import { useAddressArgs } from "./useAddressArgs.js";
|
|
31
34
|
import { APPEARANCE_ATTRIBUTE } from "./useAppearance.js";
|
|
32
35
|
import { useFrameMessages } from "./useFrameMessages.js";
|
|
36
|
+
import { usePanelDocs } from "./usePanelDocs.js";
|
|
33
37
|
import { useSystemDark } from "./useSystemDark.js";
|
|
34
38
|
/** The height of the frame before its story reports its own, in CSS px, in a page with no height of its own yet. */
|
|
35
39
|
const WAITING_HEIGHT = 240;
|
|
40
|
+
/** No actions, before the frame reported its own. */
|
|
41
|
+
const NO_ACTIONS = [];
|
|
42
|
+
/** No `argTypes`, before the frame reported its own. */
|
|
43
|
+
const NO_ARG_TYPES = {};
|
|
36
44
|
/**
|
|
37
45
|
* Tells the page around the embed how tall the story is.
|
|
38
46
|
*
|
|
@@ -98,17 +106,34 @@ const EmbedNotice = ({ message, detail, isFailure }) => {
|
|
|
98
106
|
*
|
|
99
107
|
* @param props - the story, the view, its args and the frame page.
|
|
100
108
|
*/
|
|
101
|
-
const EmbedStory = ({ location, view, panelArgs, frameUrl }) => {
|
|
109
|
+
const EmbedStory = ({ location, view, panelArgs, loadDocs, namedValues, frameUrl }) => {
|
|
102
110
|
const catalogue = useCatalogue();
|
|
103
111
|
const { story, component, source } = location;
|
|
104
112
|
const [fixed, setFixed] = useState(NOT_FIXED);
|
|
105
113
|
const [failure, setFailure] = useState(undefined);
|
|
114
|
+
const [ready, setReady] = useState(undefined);
|
|
115
|
+
const storyRef = useRef(null);
|
|
116
|
+
// the docs only serve the check: an embed whose address names no value does not load them
|
|
117
|
+
const hasValues = Object.keys(panelArgs).length > 0;
|
|
118
|
+
const docs = usePanelDocs(hasValues ? loadDocs : undefined, component.name, ready?.args, ready?.actions ?? NO_ACTIONS);
|
|
119
|
+
// whoever embeds the story writes its address, as whoever shares a link does: the same check applies
|
|
120
|
+
const addressArgs = useAddressArgs({
|
|
121
|
+
panelArgs,
|
|
122
|
+
docs,
|
|
123
|
+
storyArgs: ready?.args,
|
|
124
|
+
argTypes: ready?.argTypes ?? NO_ARG_TYPES,
|
|
125
|
+
controls: ready?.controls ?? "all",
|
|
126
|
+
namedValues,
|
|
127
|
+
frames: storyRef
|
|
128
|
+
});
|
|
106
129
|
const { heights, settled } = useFrameMessages({
|
|
107
130
|
onReady: (frame, message) => {
|
|
108
131
|
setFixed(message.fixed);
|
|
109
132
|
setFailure(undefined);
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
setReady(message);
|
|
134
|
+
const accepted = addressArgs.acceptedFor(message);
|
|
135
|
+
if (accepted !== undefined)
|
|
136
|
+
frame.postMessage(checkedArgsMessage(accepted), framesOrigin());
|
|
112
137
|
},
|
|
113
138
|
onCall: () => undefined,
|
|
114
139
|
onFailed: (_storyId, message) => setFailure(message)
|
|
@@ -135,7 +160,7 @@ const EmbedStory = ({ location, view, panelArgs, frameUrl }) => {
|
|
|
135
160
|
useHostHeight(failure === undefined ? ownHeight : undefined);
|
|
136
161
|
return (_jsxs("main", { className: "forgebook-embed", style: {
|
|
137
162
|
backgroundColor: storyBackground(source.background, shownView, catalogue, isSystemDark)
|
|
138
|
-
}, children: [failure === undefined ? null : (_jsx(EmbedNotice, { message: STRINGS.embedFailed, detail: failure, isFailure: true })), _jsx("div", { className: "forgebook-embed__story", hidden: failure !== undefined, style: { width: shownView.width === "fill" ? undefined : shownView.width }, children: _jsx(FrameView, { title: STRINGS.frameTitle(component.name, story.name), address: address, heights: heights, settled: settled, fallbackHeight: WAITING_HEIGHT, sceneHeight: Math.max(story.height ?? 0, windowHeight), fixedHeight: fixedHeight }) })] }));
|
|
163
|
+
}, children: [failure === undefined ? null : (_jsx(EmbedNotice, { message: STRINGS.embedFailed, detail: failure, isFailure: true })), _jsx("div", { ref: storyRef, className: "forgebook-embed__story", hidden: failure !== undefined, style: { width: shownView.width === "fill" ? undefined : shownView.width }, children: _jsx(FrameView, { title: STRINGS.frameTitle(component.name, story.name), address: address, heights: heights, settled: settled, fallbackHeight: WAITING_HEIGHT, sceneHeight: Math.max(story.height ?? 0, windowHeight), fixedHeight: fixedHeight }) })] }));
|
|
139
164
|
};
|
|
140
165
|
/**
|
|
141
166
|
* The page: the story the address names, or why there is none.
|
|
@@ -150,15 +175,16 @@ const EmbedContent = ({ title, sources, frameUrl, addressTexts }) => {
|
|
|
150
175
|
const storyId = params.get("story");
|
|
151
176
|
const location = storyId === null ? undefined : findStory(tree, storyId);
|
|
152
177
|
const name = location?.component.name;
|
|
178
|
+
// one object for as long as the address is read: the check is not run again for each render
|
|
179
|
+
const panelArgs = useMemo(() => addressArgsFor(parsePanelArgsParam(params.get("args")), addressTexts), [params, addressTexts]);
|
|
153
180
|
useEffect(() => {
|
|
154
181
|
document.title = name === undefined ? title : `${name} · ${title}`;
|
|
155
182
|
}, [name, title]);
|
|
156
183
|
if (location === undefined) {
|
|
157
184
|
return (_jsx("main", { className: "forgebook-embed", children: _jsx(EmbedNotice, { message: storyId === null ? LATER_STRINGS.embedNoStory : LATER_STRINGS.embedUnknownStory(storyId) }) }));
|
|
158
185
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
panelArgs: addressArgsFor(parsePanelArgsParam(params.get("args")), addressTexts), frameUrl: frameUrl }));
|
|
186
|
+
const source = sources.find((each) => each.id === location.source.id);
|
|
187
|
+
return (_jsx(EmbedStory, { location: location, view: parseView(params.toString(), catalogue), panelArgs: panelArgs, loadDocs: source === undefined ? undefined : sourceFiles(source).docs[location.story.file], namedValues: source?.namedValues, frameUrl: frameUrl }));
|
|
162
188
|
};
|
|
163
189
|
/**
|
|
164
190
|
* One story alone, inside the catalogue's settings.
|
|
@@ -5,6 +5,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { useCallback, useMemo, useRef } from "react";
|
|
8
|
+
import { NO_ACCEPTED_ARGS } from "../addressArgs.js";
|
|
8
9
|
import { findStory } from "../storyTree.js";
|
|
9
10
|
import { frameAddress, isComparing, liveView } from "../viewState.js";
|
|
10
11
|
import { showAsideStory } from "./asideStory.js";
|
|
@@ -29,7 +30,7 @@ export const PluginBlockHost = ({ load, pluginId, argument, body, page }) => {
|
|
|
29
30
|
address: frameAddress(frameUrl, options.storyId, view, switches),
|
|
30
31
|
width: "fill",
|
|
31
32
|
background: backgroundOf(location.source),
|
|
32
|
-
args:
|
|
33
|
+
args: NO_ACCEPTED_ARGS,
|
|
33
34
|
view: isComparing(view) ? undefined : liveView(view)
|
|
34
35
|
});
|
|
35
36
|
}, []);
|
|
@@ -5,7 +5,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
5
5
|
*
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
8
|
-
import { useCallback, useMemo, useRef } from "react";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
9
9
|
import { showAsideStory } from "./asideStory.js";
|
|
10
10
|
import { PluginViewHost, pluginText, useShellAppearance } from "./PluginViewHost.js";
|
|
11
11
|
/**
|
|
@@ -18,7 +18,24 @@ export const PluginTabHost = ({ load, pluginId, strings, story, report, aside })
|
|
|
18
18
|
// read when the view asks: the stage of that moment, without a new context for every change of the stage
|
|
19
19
|
const asideRef = useRef(aside);
|
|
20
20
|
asideRef.current = aside;
|
|
21
|
-
const
|
|
21
|
+
const asideFrames = useRef(new Set());
|
|
22
|
+
const showStory = useCallback((target, { title }) => {
|
|
23
|
+
const frame = showAsideStory(target, title, asideRef.current);
|
|
24
|
+
asideFrames.current.add(frame);
|
|
25
|
+
return {
|
|
26
|
+
remove: () => {
|
|
27
|
+
asideFrames.current.delete(frame);
|
|
28
|
+
frame.remove();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
const { args } = aside;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (args === undefined)
|
|
35
|
+
return;
|
|
36
|
+
for (const frame of asideFrames.current)
|
|
37
|
+
frame.settle(args);
|
|
38
|
+
}, [args]);
|
|
22
39
|
const text = useMemo(() => pluginText(strings, pluginId), [strings, pluginId]);
|
|
23
40
|
const context = useMemo(() => ({ appearance, text, story, report, showStory }), [appearance, text, story, report, showStory]);
|
|
24
41
|
return _jsx(PluginViewHost, { load: load, context: context });
|
|
@@ -18,10 +18,9 @@ import { searchables, searchCatalogue } from "../search.js";
|
|
|
18
18
|
import { SEARCH_STRINGS } from "./searchStrings.js";
|
|
19
19
|
import { shownTree } from "./treeSearch.js";
|
|
20
20
|
import { useSearchIndex } from "./useSearchIndex.js";
|
|
21
|
+
import { SIDE_BY_SIDE_WIDTH } from "./useSidebarLayout.js";
|
|
21
22
|
/** How much a page shows: what a person reads before refining the words. */
|
|
22
23
|
const LIMITS = { results: 24, hits: 3 };
|
|
23
|
-
/** The narrowest window where the list opens beside the sidebar: a smaller one has it over the sidebar, under the field. */
|
|
24
|
-
const BESIDE_WIDTH = 720;
|
|
25
24
|
/** The room between the field and the list when the list opens under it, in CSS px. */
|
|
26
25
|
const LIST_GAP = 6;
|
|
27
26
|
/** The room the list leaves around it, in CSS px. */
|
|
@@ -35,7 +34,7 @@ const LIST_WIDTH = "38rem";
|
|
|
35
34
|
* @param sidebarWidth - the sidebar's width, in CSS px.
|
|
36
35
|
*/
|
|
37
36
|
const listPlace = (field, sidebarWidth) => {
|
|
38
|
-
if (window.innerWidth <
|
|
37
|
+
if (window.innerWidth < SIDE_BY_SIDE_WIDTH)
|
|
39
38
|
return { top: field.bottom + LIST_GAP, insetInline: LIST_MARGIN };
|
|
40
39
|
const start = sidebarWidth + LIST_MARGIN;
|
|
41
40
|
return {
|
package/dist/src/shell/Shell.js
CHANGED
|
@@ -19,7 +19,7 @@ import { LogicMark } from "./LogicMark.js";
|
|
|
19
19
|
import { lazyModule } from "./lazyModule.js";
|
|
20
20
|
import { PluginHostsContext } from "./pluginHostsContext.js";
|
|
21
21
|
import { Sidebar } from "./Sidebar.js";
|
|
22
|
-
import {
|
|
22
|
+
import { SidebarEdge } from "./SidebarHandle.js";
|
|
23
23
|
import { Stage } from "./Stage.js";
|
|
24
24
|
import { StoryPicker } from "./StoryPicker.js";
|
|
25
25
|
import { STRINGS } from "./strings.js";
|
|
@@ -28,6 +28,7 @@ import { useAppearance } from "./useAppearance.js";
|
|
|
28
28
|
import { useRecentPages } from "./useRecentPages.js";
|
|
29
29
|
import { useShownView } from "./useShownView.js";
|
|
30
30
|
import { useSidebarLayout } from "./useSidebarLayout.js";
|
|
31
|
+
import { useSidebarOverlay } from "./useSidebarOverlay.js";
|
|
31
32
|
import { useStoryHeights } from "./useStoryHeights.js";
|
|
32
33
|
import { useStoryIdsElement } from "./useStoryIdsElement.js";
|
|
33
34
|
import { useStoryKeys } from "./useStoryKeys.js";
|
|
@@ -222,7 +223,7 @@ const fixedOfShown = (fixedViews, shown) => shown.kind === "story" ? (fixedViews
|
|
|
222
223
|
*
|
|
223
224
|
* @param props - whether the home is shown, and the toolbar's props.
|
|
224
225
|
*/
|
|
225
|
-
const ShellBar = ({ isHome, ...toolbar }) => isHome ? (_jsx("div", { className: "forgebook-toolbar", children: toolbar.onShowSidebar === undefined ? null : (_jsx(ShowSidebarButton, { onShowSidebar: toolbar.onShowSidebar })) })) : (_jsx(Toolbar, { ...toolbar }));
|
|
226
|
+
const ShellBar = ({ isHome, ...toolbar }) => isHome ? (_jsx("div", { className: "forgebook-toolbar", children: toolbar.onShowSidebar === undefined ? null : (_jsx(ShowSidebarButton, { onShowSidebar: toolbar.onShowSidebar, showSidebarRef: toolbar.showSidebarRef })) })) : (_jsx(Toolbar, { ...toolbar }));
|
|
226
227
|
/**
|
|
227
228
|
* What a component's page loads: its first file's docs and page, as Storybook's docs come from the first file of a
|
|
228
229
|
* title, and the code of every file, for each example's Code card.
|
|
@@ -415,10 +416,16 @@ const ShellPage = ({ title, logo, hasBranding, heights, sources, frameUrl, addre
|
|
|
415
416
|
// the written home's module, loaded when the home is first opened
|
|
416
417
|
const writtenHome = useMemo(() => (home === undefined ? undefined : lazyModule(home)), [home]);
|
|
417
418
|
const idsByTitle = useMemo(() => componentIdsByTitle(tree), [tree]);
|
|
418
|
-
const { width: sidebarWidth, setWidth: setSidebarWidth, isOpen: isSidebarOpen, setIsOpen: setIsSidebarOpen } = useSidebarLayout();
|
|
419
|
-
const showSidebar = useCallback(() => setIsSidebarOpen(true), [setIsSidebarOpen]);
|
|
420
419
|
const catalogue = useCatalogue();
|
|
421
420
|
const { address, visit, pageAddress, openPage, openFound, openAnchor, replaceAddress } = useAddress(catalogue, addressTexts);
|
|
421
|
+
const { width: sidebarWidth, setWidth: setSidebarWidth, isOpen: isSidebarOpen, setIsOpen: setIsSidebarOpen, isOverlay: isSidebarOverlay } = useSidebarLayout(visit);
|
|
422
|
+
const showSidebar = useCallback(() => setIsSidebarOpen(true), [setIsSidebarOpen]);
|
|
423
|
+
const hideSidebar = useCallback(() => setIsSidebarOpen(false), [setIsSidebarOpen]);
|
|
424
|
+
const { isOver: isSidebarOver, sidebarRef, hideButtonRef, showButtonRef } = useSidebarOverlay({
|
|
425
|
+
isOverlay: isSidebarOverlay,
|
|
426
|
+
isOpen: isSidebarOpen,
|
|
427
|
+
onClose: hideSidebar
|
|
428
|
+
});
|
|
422
429
|
// each page's last scroll position, kept across the component pages and the stage
|
|
423
430
|
const scrollPositions = useRef(new Map());
|
|
424
431
|
const scroll = { visit, positions: scrollPositions.current };
|
|
@@ -489,7 +496,9 @@ const ShellPage = ({ title, logo, hasBranding, heights, sources, frameUrl, addre
|
|
|
489
496
|
header,
|
|
490
497
|
scroll
|
|
491
498
|
};
|
|
492
|
-
return (_jsxs("div", { className: "catalogue", children: [_jsx(Sidebar, { width: sidebarWidth, isOpen: isSidebarOpen,
|
|
499
|
+
return (_jsxs("div", { className: "catalogue", children: [_jsx(Sidebar, { width: sidebarWidth, isOpen: isSidebarOpen, isOverlay: isSidebarOverlay, sidebarRef: sidebarRef, hideButtonRef: hideButtonRef, onHide: hideSidebar, onShow: showSidebar, appearance: appearance, title: title, logo: logo, hasBranding: hasBranding !== false, tree: tree, sources: sources, searchHref: (target) => addressQuery(pageAddress(target), catalogue), onOpenResult: openFound, isHome: shown.kind === "home", homeHref: addressQuery(pageAddress(HOME), catalogue), onOpenHome: () => openPage(HOME), current: sidebarCurrent(location, shownStoryId), currentPageId: shownPageLocation?.page.id, pageHref: (id) => addressQuery(pageAddress({ kind: "page", id }), catalogue), onOpenPage: (id) => openPage({ kind: "page", id }), componentHref: (sourceId, componentTitle) => addressQuery(pageAddress(titledPage(idsByTitle, sourceId, componentTitle)), catalogue), onOpenComponent: (sourceId, componentTitle) => openPage(titledPage(idsByTitle, sourceId, componentTitle)) }), _jsx(SidebarEdge, { isOverlay: isSidebarOverlay, isOpen: isSidebarOpen, width: sidebarWidth, onWidthChange: setSidebarWidth, onHide: hideSidebar }), _jsxs("main", { className: "forgebook-main", inert: isSidebarOver, children: [_jsx(ShellBar, { isHome: hasNoStories(shown), view: shownView, onViewChange: changeShownView, isStoryPage: shown.kind === "story", isComponentPage: shown.kind === "component", isLogic: isLogicPage(shown),
|
|
500
|
+
// under a sidebar lying over the page the button stays, so the bar's controls never move behind it
|
|
501
|
+
onShowSidebar: isSidebarOpen && !isSidebarOver ? undefined : showSidebar, showSidebarRef: showButtonRef, shownWidth: shown.kind === "story" ? shownWidth : undefined, fixed: fixedOfShown(fixedViews, shown), deviceDefault: deviceOfShown(catalogue.device, shown) }), shown.kind === "component" ? (_jsx(LazyPage, { module: COMPONENT_PAGE, header: header, pageProps: {
|
|
493
502
|
component: shown.location.component,
|
|
494
503
|
anchor: shown.anchor,
|
|
495
504
|
view: address.view,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
import type { StorySource } from "@forgebook/story-format/source";
|
|
14
|
-
import { type ReactNode } from "react";
|
|
14
|
+
import { type ReactNode, type Ref } from "react";
|
|
15
15
|
import type { SearchTarget } from "../search.ts";
|
|
16
16
|
import type { StoryTree } from "../storyTree.ts";
|
|
17
17
|
import { type SidebarPageLinks } from "./SidebarDocs.tsx";
|
|
@@ -54,6 +54,12 @@ export type SidebarProps = SidebarPageLinks & {
|
|
|
54
54
|
readonly width: number;
|
|
55
55
|
/** Whether it is shown: hidden, it slides out and stays mounted, so its search and open families come back with it. */
|
|
56
56
|
readonly isOpen: boolean;
|
|
57
|
+
/** Whether it lies over the page and not beside it, on a narrow window. */
|
|
58
|
+
readonly isOverlay: boolean;
|
|
59
|
+
/** The sidebar's element. */
|
|
60
|
+
readonly sidebarRef: Ref<HTMLElement>;
|
|
61
|
+
/** Its button that hides it. */
|
|
62
|
+
readonly hideButtonRef: Ref<HTMLButtonElement>;
|
|
57
63
|
/** Hides the sidebar. */
|
|
58
64
|
readonly onHide: () => void;
|
|
59
65
|
/** Shows it again. */
|
|
@@ -66,4 +72,4 @@ export type SidebarProps = SidebarPageLinks & {
|
|
|
66
72
|
*
|
|
67
73
|
* @param props - the tree, the shown component and how to open one.
|
|
68
74
|
*/
|
|
69
|
-
export declare const Sidebar: ({ title, logo, hasBranding, tree, sources: storySources, searchHref, onOpenResult, isHome, homeHref, onOpenHome, current, currentPageId, pageHref, onOpenPage, width, isOpen, onHide, onShow, appearance, ...links }: SidebarProps) => ReactNode;
|
|
75
|
+
export declare const Sidebar: ({ title, logo, hasBranding, tree, sources: storySources, searchHref, onOpenResult, isHome, homeHref, onOpenHome, current, currentPageId, pageHref, onOpenPage, width, isOpen, isOverlay, sidebarRef, hideButtonRef, onHide, onShow, appearance, ...links }: SidebarProps) => ReactNode;
|
|
@@ -50,7 +50,7 @@ const componentCount = (family) => family.groups.reduce((count, group) => count
|
|
|
50
50
|
*
|
|
51
51
|
* @param props - the tree, the shown component and how to open one.
|
|
52
52
|
*/
|
|
53
|
-
export const Sidebar = ({ title, logo, hasBranding, tree, sources: storySources, searchHref, onOpenResult, isHome, homeHref, onOpenHome, current, currentPageId, pageHref, onOpenPage, width, isOpen, onHide, onShow, appearance, ...links }) => {
|
|
53
|
+
export const Sidebar = ({ title, logo, hasBranding, tree, sources: storySources, searchHref, onOpenResult, isHome, homeHref, onOpenHome, current, currentPageId, pageHref, onOpenPage, width, isOpen, isOverlay, sidebarRef, hideButtonRef, onHide, onShow, appearance, ...links }) => {
|
|
54
54
|
const [query, setQuery] = useState("");
|
|
55
55
|
const [filterTree, setFilterTree] = useState();
|
|
56
56
|
const showFiltered = useCallback((filter) => setFilterTree(() => filter), []);
|
|
@@ -86,12 +86,12 @@ export const Sidebar = ({ title, logo, hasBranding, tree, sources: storySources,
|
|
|
86
86
|
// the filter comes with the search's list; a moment before it, or with no words, the tree shows whole
|
|
87
87
|
const { sources, pages } = isSearching && filterTree !== undefined ? filterTree(tree, query) : wholeTree(tree);
|
|
88
88
|
const hasMatches = pages.length > 0 || sources.some(({ families }) => families.length > 0);
|
|
89
|
-
return (_jsxs("aside", { className: "forgebook-sidebar", "data-open": isOpen ? "" : undefined, inert: !isOpen, style: { width, marginInlineStart: isOpen ? 0 : -width }, children: [_jsxs("div", { className: "forgebook-brand", children: [_jsxs("a", { className: "forgebook-brand__home", href: homeHref, "aria-current": isHome ? "page" : undefined, title: STRINGS.home, onClick: (event) => {
|
|
89
|
+
return (_jsxs("aside", { ref: sidebarRef, className: "forgebook-sidebar", "data-open": isOpen ? "" : undefined, "data-overlay": isOverlay ? "" : undefined, inert: !isOpen, style: { width, marginInlineStart: isOpen ? 0 : -width }, children: [_jsxs("div", { className: "forgebook-brand", children: [_jsxs("a", { className: "forgebook-brand__home", href: homeHref, "aria-current": isHome ? "page" : undefined, title: STRINGS.home, onClick: (event) => {
|
|
90
90
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0)
|
|
91
91
|
return;
|
|
92
92
|
event.preventDefault();
|
|
93
93
|
onOpenHome();
|
|
94
|
-
}, children: [mark === undefined ? null : (_jsx("img", { className: "forgebook-brand__logo", src: mark, alt: "", width: 28, height: 28 })), _jsx("span", { className: "forgebook-brand__name", children: title })] }), _jsx("button", { type: "button", className: "forgebook-icon-button forgebook-brand__hide", title: STRINGS.hideSidebar, "aria-label": STRINGS.hideSidebar, onClick: onHide, children: _jsx(Icon, { name: "sidebar", size: 14 }) })] }), _jsx(SidebarSearch, { query: query, onQueryChange: setQuery, onFilterReady: showFiltered, tree: tree, sources: storySources, sidebarWidth: width, isSidebarOpen: isOpen, onShowSidebar: onShow, searchHref: searchHref, onOpenResult: onOpenResult }), _jsxs("nav", { ref: navRef, className: "forgebook-tree", "aria-label": STRINGS.componentsNav, children: [hasMatches ? null : _jsx("p", { className: "forgebook-tree__empty", children: STRINGS.noMatch }), _jsx(SidebarDocs, { sources: pages, currentPageId: currentPageId, isSearching: isSearching, foldChoices: foldChoices, onFoldChange: setFoldOpen, pageHref: pageHref, onOpenPage: onOpenPage }), sources.map(({ source, families }) => families.length === 0 ? null : (_jsxs("section", { className: "forgebook-tree__source", children: [_jsxs("h2", { className: "forgebook-tree__source-name", children: [source.logo === undefined ? null : (_jsx("img", { className: "forgebook-source-logo", src: source.logo, alt: "", width: 14, height: 14 })), source.name] }), families.map((family) => {
|
|
94
|
+
}, children: [mark === undefined ? null : (_jsx("img", { className: "forgebook-brand__logo", src: mark, alt: "", width: 28, height: 28 })), _jsx("span", { className: "forgebook-brand__name", children: title })] }), _jsx("button", { ref: hideButtonRef, type: "button", className: "forgebook-icon-button forgebook-brand__hide", title: STRINGS.hideSidebar, "aria-label": STRINGS.hideSidebar, onClick: onHide, children: _jsx(Icon, { name: "sidebar", size: 14 }) })] }), _jsx(SidebarSearch, { query: query, onQueryChange: setQuery, onFilterReady: showFiltered, tree: tree, sources: storySources, sidebarWidth: width, isSidebarOpen: isOpen, onShowSidebar: onShow, searchHref: searchHref, onOpenResult: onOpenResult }), _jsxs("nav", { ref: navRef, className: "forgebook-tree", "aria-label": STRINGS.componentsNav, children: [hasMatches ? null : _jsx("p", { className: "forgebook-tree__empty", children: STRINGS.noMatch }), _jsx(SidebarDocs, { sources: pages, currentPageId: currentPageId, isSearching: isSearching, foldChoices: foldChoices, onFoldChange: setFoldOpen, pageHref: pageHref, onOpenPage: onOpenPage }), sources.map(({ source, families }) => families.length === 0 ? null : (_jsxs("section", { className: "forgebook-tree__source", children: [_jsxs("h2", { className: "forgebook-tree__source-name", children: [source.logo === undefined ? null : (_jsx("img", { className: "forgebook-source-logo", src: source.logo, alt: "", width: 14, height: 14 })), source.name] }), families.map((family) => {
|
|
95
95
|
const key = familyKey(source, family);
|
|
96
96
|
// a search opens every family it finds something in
|
|
97
97
|
const isOpen = isSearching || (foldChoices.get(key) ?? isCurrentFamily(source, family));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The handle on the sidebar's right edge: dragged, or moved with the arrow keys, it sets the sidebar's width
|
|
2
|
+
* The handle on the sidebar's right edge: dragged, or moved with the arrow keys, it sets the sidebar's width; on a narrow
|
|
3
|
+
* window, where the sidebar lies over the page at no chosen width, the shade on the page takes its place.
|
|
3
4
|
*
|
|
4
5
|
* @module
|
|
5
6
|
*/
|
|
@@ -26,3 +27,15 @@ export declare const SidebarHandle: ({ width, onWidthChange }: {
|
|
|
26
27
|
readonly width: number;
|
|
27
28
|
readonly onWidthChange: (width: number) => void;
|
|
28
29
|
}) => ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* What borders the sidebar: beside the page its handle, while shown; over it the shade on the page.
|
|
32
|
+
*
|
|
33
|
+
* @param props - where the sidebar lies, whether it is shown, its width and how to hide it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SidebarEdge: ({ isOverlay, isOpen, width, onWidthChange, onHide }: {
|
|
36
|
+
readonly isOverlay: boolean;
|
|
37
|
+
readonly isOpen: boolean;
|
|
38
|
+
readonly width: number;
|
|
39
|
+
readonly onWidthChange: (width: number) => void;
|
|
40
|
+
readonly onHide: () => void;
|
|
41
|
+
}) => ReactNode;
|
|
@@ -43,3 +43,13 @@ export const SidebarHandle = ({ width, onWidthChange }) => {
|
|
|
43
43
|
};
|
|
44
44
|
return (_jsx("div", { className: "forgebook-sidebar-handle", role: "separator", "aria-orientation": "vertical", "aria-label": STRINGS.sidebarWidth, "aria-valuenow": width, "aria-valuemin": SIDEBAR_WIDTH.min, "aria-valuemax": SIDEBAR_WIDTH.max, tabIndex: 0, onPointerDown: startResize, onKeyDown: resizeWithKeys }));
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* What borders the sidebar: beside the page its handle, while shown; over it the shade on the page.
|
|
48
|
+
*
|
|
49
|
+
* @param props - where the sidebar lies, whether it is shown, its width and how to hide it.
|
|
50
|
+
*/
|
|
51
|
+
export const SidebarEdge = ({ isOverlay, isOpen, width, onWidthChange, onHide }) => {
|
|
52
|
+
if (!isOverlay)
|
|
53
|
+
return isOpen ? _jsx(SidebarHandle, { width: width, onWidthChange: onWidthChange }) : null;
|
|
54
|
+
return (_jsx("div", { className: "forgebook-sidebar-shade", "data-open": isOpen ? "" : undefined, "aria-hidden": "true", onClick: onHide }));
|
|
55
|
+
};
|
|
@@ -68,13 +68,16 @@ export const SidebarSearch = ({ query, onQueryChange, onFilterReady, tree, sourc
|
|
|
68
68
|
event.preventDefault();
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
// Escape empties the search first, then leaves the field
|
|
71
|
+
// Escape empties the search first, then leaves the field; emptying uses the key up, so a sidebar lying over
|
|
72
|
+
// the page stays open for it (`useSidebarOverlay.ts`)
|
|
72
73
|
if (event.key !== "Escape")
|
|
73
74
|
return;
|
|
74
|
-
if (query === "")
|
|
75
|
+
if (query === "") {
|
|
75
76
|
event.currentTarget.blur();
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
onQueryChange("");
|
|
78
81
|
}, placeholder: STRINGS.searchPlaceholder, "aria-label": STRINGS.searchLabel, onChange: (event) => {
|
|
79
82
|
onQueryChange(event.target.value);
|
|
80
83
|
setIsListOpen(true);
|
package/dist/src/shell/Stage.js
CHANGED
|
@@ -12,9 +12,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { checkedArgsMessage } from "../addressArgs.js";
|
|
15
16
|
import { panelProps, settableProps } from "../argTypes.js";
|
|
16
17
|
import { matrixOf, matrixProps } from "../matrix.js";
|
|
17
|
-
import { panelArgsParam
|
|
18
|
+
import { panelArgsParam } from "../messages.js";
|
|
18
19
|
import { withFixed, withOwnFixed } from "../storyGlobals.js";
|
|
19
20
|
import { frameAddress, isComparing, liveView, schemeOf, viewParams } from "../viewState.js";
|
|
20
21
|
import { useStoryClockControls } from "./ClockControls.js";
|
|
@@ -127,7 +128,8 @@ export const Stage = ({ component, story, view: ownView, onViewChange: onOwnView
|
|
|
127
128
|
storyArgs: storyArgs.get(story.id),
|
|
128
129
|
argTypes: panel.argTypes,
|
|
129
130
|
controls: panel.controls,
|
|
130
|
-
namedValues
|
|
131
|
+
namedValues,
|
|
132
|
+
frames: storyRef
|
|
131
133
|
});
|
|
132
134
|
const acceptedArgs = addressArgs.accepted;
|
|
133
135
|
const values = { ...storyArgs.get(story.id), ...acceptedArgs };
|
|
@@ -160,7 +162,7 @@ export const Stage = ({ component, story, view: ownView, onViewChange: onOwnView
|
|
|
160
162
|
// checked against the shown story's declaration only: a late message of the story shown before sends nothing
|
|
161
163
|
const accepted = message.storyId === story.id ? addressArgs.acceptedFor(message) : undefined;
|
|
162
164
|
if (accepted !== undefined)
|
|
163
|
-
send(
|
|
165
|
+
send(checkedArgsMessage(accepted));
|
|
164
166
|
send({ type: "forgebook:matrix", matrix });
|
|
165
167
|
// the view may have changed while the frame loaded
|
|
166
168
|
if (!isComparing(view))
|
|
@@ -186,14 +188,6 @@ export const Stage = ({ component, story, view: ownView, onViewChange: onOwnView
|
|
|
186
188
|
// the docs may arrive after the frames: they learn the events then
|
|
187
189
|
useEffect(() => tellFrames(storyRef.current, { type: "forgebook:events", names: eventNames }), [eventNames]);
|
|
188
190
|
useEffect(() => tellFrames(storyRef.current, { type: "forgebook:matrix", matrix }), [matrix]);
|
|
189
|
-
// the values reach the frames from the address, so going back in the history sets them too; compared as text, so
|
|
190
|
-
// a frame is told again only when they change
|
|
191
|
-
const { acceptedParam } = addressArgs;
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (acceptedParam === undefined)
|
|
194
|
-
return;
|
|
195
|
-
tellFrames(storyRef.current, { type: "forgebook:args", args: parsePanelArgsParam(acceptedParam) });
|
|
196
|
-
}, [acceptedParam]);
|
|
197
191
|
// a change starts from the values the story takes: one the address named and the story does not is let go
|
|
198
192
|
const changeValue = (name, value) => {
|
|
199
193
|
const { [name]: _removed, ...rest } = acceptedArgs;
|
|
@@ -235,7 +229,8 @@ export const Stage = ({ component, story, view: ownView, onViewChange: onOwnView
|
|
|
235
229
|
address: deviceAddress(frameAddress(frameUrl, story.id, view, switches), framed),
|
|
236
230
|
width: framed?.area.width ?? view.width,
|
|
237
231
|
background: backgroundOf(view),
|
|
238
|
-
|
|
232
|
+
// as the stage's frames receive them: an aside asked for while the check waits for the docs is told when it ends
|
|
233
|
+
args: addressArgs.checked,
|
|
239
234
|
view: liveView(view)
|
|
240
235
|
}
|
|
241
236
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
import { type ReactNode } from "react";
|
|
12
|
+
import { type ReactNode, type Ref } from "react";
|
|
13
13
|
import { type DeviceDefault } from "../deviceView.ts";
|
|
14
14
|
import { type FixedView } from "../storyGlobals.ts";
|
|
15
15
|
import { type ViewState } from "../viewState.ts";
|
|
@@ -23,8 +23,10 @@ export type ToolbarProps = {
|
|
|
23
23
|
readonly isStoryPage?: boolean;
|
|
24
24
|
/** Whether a component's page is shown: the audience changes nothing elsewhere. */
|
|
25
25
|
readonly isComponentPage?: boolean;
|
|
26
|
-
/** Shows the hidden sidebar; none while it is shown. */
|
|
26
|
+
/** Shows the hidden sidebar; none while it is shown beside the page. */
|
|
27
27
|
readonly onShowSidebar?: (() => void) | undefined;
|
|
28
|
+
/** The button that shows the sidebar, where the focus goes back when a sidebar over the page closes. */
|
|
29
|
+
readonly showSidebarRef?: Ref<HTMLButtonElement> | undefined;
|
|
28
30
|
/** The width the shown story really has, in CSS px, for the width field; none on a component's page. */
|
|
29
31
|
readonly shownWidth?: number | undefined;
|
|
30
32
|
/** What the shown story fixes of the view: shown, and not changeable here; nothing by default. */
|
|
@@ -39,7 +41,7 @@ export type ToolbarProps = {
|
|
|
39
41
|
*
|
|
40
42
|
* @param props - what showing the sidebar does.
|
|
41
43
|
*/
|
|
42
|
-
export declare const ShowSidebarButton: ({ onShowSidebar }: {
|
|
44
|
+
export declare const ShowSidebarButton: ({ onShowSidebar, showSidebarRef }: Pick<ToolbarProps, "showSidebarRef"> & {
|
|
43
45
|
readonly onShowSidebar: () => void;
|
|
44
46
|
}) => ReactNode;
|
|
45
47
|
/**
|
|
@@ -47,4 +49,4 @@ export declare const ShowSidebarButton: ({ onShowSidebar }: {
|
|
|
47
49
|
*
|
|
48
50
|
* @param props - the view, its setter, the page shown and the story's width.
|
|
49
51
|
*/
|
|
50
|
-
export declare const Toolbar: ({ view: ownView, onViewChange: onOwnViewChange, isStoryPage, isComponentPage, onShowSidebar, shownWidth, fixed, isLogic, deviceDefault }: ToolbarProps) => ReactNode;
|
|
52
|
+
export declare const Toolbar: ({ view: ownView, onViewChange: onOwnViewChange, isStoryPage, isComponentPage, onShowSidebar, showSidebarRef, shownWidth, fixed, isLogic, deviceDefault }: ToolbarProps) => ReactNode;
|