forgebook 0.3.2 → 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 +3 -2
- package/dist/bin/forgebook/agents.md +3 -3
- 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/Stage.js +7 -12
- package/dist/src/shell/asideStory.d.ts +17 -4
- package/dist/src/shell/asideStory.js +14 -2
- package/dist/src/shell/useAddressArgs.d.ts +18 -2
- package/dist/src/shell/useAddressArgs.js +19 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1095,8 +1095,9 @@ it, and the Embed button's **Follow the story's height** adds the few lines that
|
|
|
1095
1095
|
iframe, since its modules are loaded as those of its own origin.
|
|
1096
1096
|
|
|
1097
1097
|
An embed sends nothing but the height and listens to nothing from the page around it, and it keeps working where a browser
|
|
1098
|
-
denies a third-party frame its storage. What it does take from outside is its address
|
|
1099
|
-
|
|
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.
|
|
1100
1101
|
And a height says something: a page that can reach your catalogue can tell whether a story exists.
|
|
1101
1102
|
|
|
1102
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:
|
|
@@ -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 });
|
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
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
-
import { type
|
|
10
|
+
import { type AcceptedArgs } from "../addressArgs.ts";
|
|
11
11
|
import type { PluginStoryFrame } from "../plugin.ts";
|
|
12
12
|
import type { LiveView, StageWidth } from "../viewState.ts";
|
|
13
13
|
/** How the story is drawn again. */
|
|
@@ -18,11 +18,24 @@ export type AsideStory = {
|
|
|
18
18
|
readonly width: StageWidth;
|
|
19
19
|
/** The colour its page has in the view; none without one. */
|
|
20
20
|
readonly background: string | undefined;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The values the story accepted of the panel's, sent once it is ready; `undefined` while the check waits for the
|
|
23
|
+
* story's docs, when the frame draws the story's own and `AsideFrame.settle` gives them.
|
|
24
|
+
*/
|
|
25
|
+
readonly args: AcceptedArgs | undefined;
|
|
23
26
|
/** The view's live part, sent once it is ready; none when the address already says it all (the compare view). */
|
|
24
27
|
readonly view: LiveView | undefined;
|
|
25
28
|
};
|
|
29
|
+
/** A story's second frame, as the shell holds it. */
|
|
30
|
+
export type AsideFrame = PluginStoryFrame & {
|
|
31
|
+
/**
|
|
32
|
+
* Gives a frame drawn before the check ended the values it accepted; a frame that has them keeps them, since it shows
|
|
33
|
+
* the panel's values of the moment it was asked for.
|
|
34
|
+
*
|
|
35
|
+
* @param args - the accepted values.
|
|
36
|
+
*/
|
|
37
|
+
readonly settle: (args: AcceptedArgs) => void;
|
|
38
|
+
};
|
|
26
39
|
/**
|
|
27
40
|
* Draws a story's second frame in an element.
|
|
28
41
|
*
|
|
@@ -31,4 +44,4 @@ export type AsideStory = {
|
|
|
31
44
|
* @param aside - how the story is drawn.
|
|
32
45
|
* @returns what takes it away.
|
|
33
46
|
*/
|
|
34
|
-
export declare const showAsideStory: (target: HTMLElement, title: string, aside: AsideStory) =>
|
|
47
|
+
export declare const showAsideStory: (target: HTMLElement, title: string, aside: AsideStory) => AsideFrame;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
+
import { checkedArgsMessage } from "../addressArgs.js";
|
|
10
11
|
import { isFrameMessage } from "../messages.js";
|
|
11
12
|
import { framesOrigin, isFromFrames, storyFrameAttributes } from "./framesOrigin.js";
|
|
12
13
|
import { FRAME_ASIDE_ATTRIBUTE } from "./useFrameMessages.js";
|
|
@@ -32,14 +33,18 @@ export const showAsideStory = (target, title, aside) => {
|
|
|
32
33
|
frame.style.width = aside.width === "fill" ? "100%" : `${aside.width}px`;
|
|
33
34
|
if (aside.background !== undefined)
|
|
34
35
|
target.style.backgroundColor = aside.background;
|
|
36
|
+
let args = aside.args;
|
|
37
|
+
let isReady = false;
|
|
38
|
+
const send = (message) => frame.contentWindow?.postMessage(message, framesOrigin());
|
|
35
39
|
const answer = (event) => {
|
|
36
40
|
const story = frame.contentWindow;
|
|
37
41
|
if (!isFromFrames(event) || event.source !== story || story === null)
|
|
38
42
|
return;
|
|
39
43
|
if (!isFrameMessage(event.data) || event.data.type !== "forgebook:ready")
|
|
40
44
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
isReady = true;
|
|
46
|
+
if (args !== undefined)
|
|
47
|
+
send(checkedArgsMessage(args));
|
|
43
48
|
if (aside.view !== undefined)
|
|
44
49
|
send({ type: "forgebook:view", view: aside.view });
|
|
45
50
|
};
|
|
@@ -50,6 +55,13 @@ export const showAsideStory = (target, title, aside) => {
|
|
|
50
55
|
remove: () => {
|
|
51
56
|
window.removeEventListener("message", answer);
|
|
52
57
|
frame.remove();
|
|
58
|
+
},
|
|
59
|
+
settle: (accepted) => {
|
|
60
|
+
if (args !== undefined)
|
|
61
|
+
return;
|
|
62
|
+
args = accepted;
|
|
63
|
+
if (isReady)
|
|
64
|
+
send(checkedArgsMessage(accepted));
|
|
53
65
|
}
|
|
54
66
|
};
|
|
55
67
|
};
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The props panel's values an address names, as a page hands them to the story's frames: only those the story declares,
|
|
3
|
+
* with a value of the declared type, once the story's frame and its component's docs say what that is.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The address is written by whoever shares the link, so its values are checked here, in the core, before any frame
|
|
7
|
+
* receives them, whatever framework draws the story (`addressArgs.ts`). Until the story's frame is ready and its docs
|
|
8
|
+
* are read (or known to be missing), no value is sent: the frame draws the story's own args meanwhile.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type RefObject } from "react";
|
|
13
|
+
import { type AcceptedArgs, type ArgDeclaration, type DroppedArg } from "../addressArgs.ts";
|
|
2
14
|
import type { ComponentDocs } from "../componentDocs.ts";
|
|
3
15
|
import { type PanelArgs } from "../messages.ts";
|
|
4
16
|
import type { NamedValues } from "../namedValues.ts";
|
|
@@ -29,11 +41,15 @@ export type AddressArgsInput = {
|
|
|
29
41
|
readonly controls: ReadyDeclaration["controls"];
|
|
30
42
|
/** The source's named values; none when it has none. */
|
|
31
43
|
readonly namedValues: NamedValues | undefined;
|
|
44
|
+
/** The element that holds the story's frames, which hear each change of the checked values. */
|
|
45
|
+
readonly frames: RefObject<HTMLElement | null>;
|
|
32
46
|
};
|
|
33
47
|
/** The address's values once checked, and how to check them for a frame that has just reported ready. */
|
|
34
48
|
export type AddressArgs = {
|
|
35
49
|
/** The values the story receives; none before its declaration is known. */
|
|
36
50
|
readonly accepted: PanelArgs;
|
|
51
|
+
/** The same values for a frame that waits for the check; `undefined` until the declaration is known. */
|
|
52
|
+
readonly checked: AcceptedArgs | undefined;
|
|
37
53
|
/** The values left out, with the reason. */
|
|
38
54
|
readonly dropped: readonly DroppedArg[];
|
|
39
55
|
/**
|
|
@@ -45,7 +61,7 @@ export type AddressArgs = {
|
|
|
45
61
|
* The values to send a frame that has just reported ready, from its own message, since the state that holds it
|
|
46
62
|
* updates after; none while the docs load.
|
|
47
63
|
*/
|
|
48
|
-
readonly acceptedFor: (ready: ReadyDeclaration) =>
|
|
64
|
+
readonly acceptedFor: (ready: ReadyDeclaration) => AcceptedArgs | undefined;
|
|
49
65
|
};
|
|
50
66
|
/**
|
|
51
67
|
* The address's values, checked against what the story declares.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The props panel's values an address names, as
|
|
3
|
-
*
|
|
2
|
+
* The props panel's values an address names, as a page hands them to the story's frames: only those the story declares,
|
|
3
|
+
* with a value of the declared type, once the story's frame and its component's docs say what that is.
|
|
4
4
|
*
|
|
5
5
|
* @remarks
|
|
6
6
|
* The address is written by whoever shares the link, so its values are checked here, in the core, before any frame
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
import { useMemo } from "react";
|
|
13
|
-
import { checkedAddressArgs } from "../addressArgs.js";
|
|
12
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
13
|
+
import { checkedAddressArgs, checkedArgsMessage } from "../addressArgs.js";
|
|
14
14
|
import { panelArgsParam } from "../messages.js";
|
|
15
|
+
import { tellFrames } from "./FrameView.js";
|
|
15
16
|
/**
|
|
16
17
|
* What a story declares, once both its frame and its docs answered.
|
|
17
18
|
*
|
|
@@ -43,20 +44,31 @@ const NO_NAMED_VALUES = {};
|
|
|
43
44
|
* @param input - the address's values and what the stage knows of the story.
|
|
44
45
|
*/
|
|
45
46
|
export const useAddressArgs = (input) => {
|
|
46
|
-
const { panelArgs, docs, storyArgs, argTypes, controls } = input;
|
|
47
|
+
const { panelArgs, docs, storyArgs, argTypes, controls, frames } = input;
|
|
47
48
|
const namedValues = input.namedValues ?? NO_NAMED_VALUES;
|
|
48
49
|
const checked = useMemo(() => {
|
|
49
50
|
const ready = storyArgs === undefined ? undefined : { args: storyArgs, argTypes, controls };
|
|
50
51
|
const declaration = declarationOf(docs, ready, namedValues);
|
|
51
52
|
return declaration === undefined ? undefined : checkedAddressArgs(panelArgs, declaration);
|
|
52
53
|
}, [panelArgs, docs, storyArgs, argTypes, controls, namedValues]);
|
|
54
|
+
const acceptedParam = checked === undefined ? undefined : panelArgsParam(checked.accepted);
|
|
55
|
+
// the values reach the frames from the address, so going back in the history sets them too; compared as text, so a
|
|
56
|
+
// frame is told again only when they change
|
|
57
|
+
const sealedRef = useRef(checked?.sealed);
|
|
58
|
+
sealedRef.current = checked?.sealed;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (acceptedParam === undefined || sealedRef.current === undefined)
|
|
61
|
+
return;
|
|
62
|
+
tellFrames(frames.current, checkedArgsMessage(sealedRef.current));
|
|
63
|
+
}, [acceptedParam, frames]);
|
|
53
64
|
return {
|
|
54
65
|
accepted: checked?.accepted ?? NO_ARGS,
|
|
66
|
+
checked: checked?.sealed,
|
|
55
67
|
dropped: checked?.dropped ?? NOTHING_DROPPED,
|
|
56
|
-
acceptedParam
|
|
68
|
+
acceptedParam,
|
|
57
69
|
acceptedFor: (message) => {
|
|
58
70
|
const declaration = declarationOf(docs, message, namedValues);
|
|
59
|
-
return declaration === undefined ? undefined : checkedAddressArgs(panelArgs, declaration).
|
|
71
|
+
return declaration === undefined ? undefined : checkedAddressArgs(panelArgs, declaration).sealed;
|
|
60
72
|
}
|
|
61
73
|
};
|
|
62
74
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgebook",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "See every component in every theme, contrast, language and width: CSF stories, one frame per story, and a props table from the components' types.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
"lightningcss": "1.33.0",
|
|
245
245
|
"marked": "18.1.0",
|
|
246
246
|
"typescript-6": "npm:typescript@6.0.3",
|
|
247
|
-
"@forgebook/story-format": "0.2.
|
|
247
|
+
"@forgebook/story-format": "0.2.4"
|
|
248
248
|
},
|
|
249
249
|
"peerDependencies": {
|
|
250
250
|
"@testing-library/dom": "^10.4.0",
|