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 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: like the catalogue's own `args`, the
1099
- address reaches the story's props, so a story that renders a prop as raw HTML should not be published for anyone to frame.
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 props the
88
- story's panel can set, with a value of their type; to show a story with other values, write a story with those
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 {};
@@ -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
- return { accepted: Object.fromEntries(accepted), dropped };
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
- const args = { type: "forgebook:args", args: panelArgs };
111
- frame.postMessage(args, framesOrigin());
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
- return (_jsx(EmbedStory, { location: location, view: parseView(params.toString(), catalogue),
160
- // whoever embeds the story writes its address, as whoever shares a link does: the same rule applies
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 showStory = useCallback((target, { title }) => showAsideStory(target, title, asideRef.current), []);
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 });
@@ -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, parsePanelArgsParam } from "../messages.js";
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({ type: "forgebook:args", args: accepted });
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
- args: panelArgs,
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 PanelArgs } from "../messages.ts";
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
- /** The values set in the panel, sent once it is ready. */
22
- readonly args: PanelArgs;
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) => PluginStoryFrame;
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
- const send = (message) => story.postMessage(message, framesOrigin());
42
- send({ type: "forgebook:args", args: aside.args });
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
- import { type ArgDeclaration, type DroppedArg } from "../addressArgs.ts";
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) => PanelArgs | undefined;
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 the stage hands them to the story's frames: only those the story
3
- * declares, with a value of the declared type, once the story's frame and its component's docs say what that is.
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: checked === undefined ? undefined : panelArgsParam(checked.accepted),
68
+ acceptedParam,
57
69
  acceptedFor: (message) => {
58
70
  const declaration = declarationOf(docs, message, namedValues);
59
- return declaration === undefined ? undefined : checkedAddressArgs(panelArgs, declaration).accepted;
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.2",
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.3"
247
+ "@forgebook/story-format": "0.2.4"
248
248
  },
249
249
  "peerDependencies": {
250
250
  "@testing-library/dom": "^10.4.0",