forgebook 0.4.0 → 0.4.1
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 +83 -3
- package/dist/bin/forgebook/agentText.js +7 -0
- package/dist/bin/forgebook/guide/commands.md +5 -0
- package/dist/bin/forgebook/guide/stories.md +7 -2
- package/dist/bin/forgebook/verifyBase.js +10 -2
- package/dist/bin/forgebook/verifyVerdict.js +27 -1
- package/dist/src/argControl.d.ts +51 -3
- package/dist/src/argTypes.js +4 -2
- package/dist/src/componentDocs.d.ts +12 -0
- package/dist/src/componentDocs.js +5 -1
- package/dist/src/controlCode.d.ts +27 -0
- package/dist/src/controlCode.js +41 -0
- package/dist/src/frame/controlValues.d.ts +39 -0
- package/dist/src/frame/controlValues.js +143 -0
- package/dist/src/frame/mountFrame.d.ts +3 -0
- package/dist/src/frame/mountFrame.js +8 -10
- package/dist/src/frame/panelControls.d.ts +67 -0
- package/dist/src/frame/panelControls.js +110 -0
- package/dist/src/shell/AuthoredHome.js +3 -1
- package/dist/src/shell/CodeTab.d.ts +10 -5
- package/dist/src/shell/CodeTab.js +44 -1
- package/dist/src/shell/ComponentPage.d.ts +5 -5
- package/dist/src/shell/ComponentPage.js +4 -2
- package/dist/src/shell/CustomControl.js +14 -21
- package/dist/src/shell/HomePage.js +5 -3
- package/dist/src/shell/PageWidth.d.ts +29 -0
- package/dist/src/shell/PageWidth.js +18 -0
- package/dist/src/shell/PropRows.d.ts +2 -12
- package/dist/src/shell/PropRows.js +6 -7
- package/dist/src/shell/PropsTab.js +2 -3
- package/dist/src/shell/Shell.js +5 -2
- package/dist/src/shell/StoryPicker.d.ts +4 -2
- package/dist/src/shell/StoryPicker.js +3 -3
- package/dist/src/shell/Switch.d.ts +29 -0
- package/dist/src/shell/Switch.js +11 -0
- package/dist/src/shell/customControls.d.ts +7 -0
- package/dist/src/shell/customControls.js +12 -1
- package/dist/src/shell/dockTabs.js +2 -1
- package/dist/src/shell/frameOrder.d.ts +35 -0
- package/dist/src/shell/frameOrder.js +45 -0
- package/dist/src/shell/laterStrings.d.ts +1 -0
- package/dist/src/shell/laterStrings.js +1 -0
- package/dist/src/shell/shell.css +36 -9
- package/dist/src/shell/useFrameQueue.d.ts +8 -3
- package/dist/src/shell/useFrameQueue.js +42 -12
- package/dist/src/shell/useScrollSettled.d.ts +18 -0
- package/dist/src/shell/useScrollSettled.js +56 -0
- package/dist/src/shell/valueEditors/ValueField.js +2 -1
- package/dist/src/storyCheck.d.ts +14 -0
- package/dist/src/storyCheck.js +16 -0
- package/dist/src/storyCodeText.d.ts +8 -2
- package/dist/src/storyCodeText.js +8 -5
- package/dist/src/storyControls.d.ts +3 -0
- package/dist/src/vite/agent.types.d.ts +8 -1
- package/dist/src/vite/agentRoutes.js +38 -10
- package/dist/src/vite/agentStoryCode.d.ts +3 -0
- package/dist/src/vite/agentStoryCode.js +8 -2
- package/dist/src/vite/argControlsConfig.d.ts +30 -0
- package/dist/src/vite/argControlsConfig.js +176 -0
- package/dist/src/vite/catalogueConfig.js +6 -1
- package/dist/src/vite/config.d.ts +38 -3
- package/dist/src/vite/configFile.d.ts +3 -2
- package/dist/src/vite/configFile.js +6 -21
- package/dist/src/vite/configKeys.js +2 -1
- package/dist/src/vite/forgebookPlugin.js +3 -1
- package/dist/src/vite/pages.js +12 -1
- package/dist/src/vite/propDocs.js +58 -3
- package/dist/src/vite/typedControls.d.ts +31 -0
- package/dist/src/vite/typedControls.js +66 -0
- package/package.json +2 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { isArgValue, isUnsetArg } from "../messages.js";
|
|
2
|
+
import { panelArgTypesOf } from "../storyArgTypes.js";
|
|
3
|
+
/** Whether a value is a plain object. */
|
|
4
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
/**
|
|
6
|
+
* The controls the catalogue gives the component's props by type, by prop, from its docs as the plugin served them.
|
|
7
|
+
*
|
|
8
|
+
* @param docs - the docs module's value.
|
|
9
|
+
*/
|
|
10
|
+
const typedControlsOf = (docs) => {
|
|
11
|
+
const props = isRecord(docs) && Array.isArray(docs["props"]) ? docs["props"] : [];
|
|
12
|
+
return Object.fromEntries(props.flatMap((prop) => isRecord(prop) && typeof prop["name"] === "string" && typeof prop["customControl"] === "string"
|
|
13
|
+
? [[prop["name"], prop["customControl"]]]
|
|
14
|
+
: []));
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The custom control of each arg, as the panel draws it: a control the story's `argTypes` name, else the one the
|
|
18
|
+
* catalogue gives the prop by type, unless the `argTypes` name another control for it.
|
|
19
|
+
*
|
|
20
|
+
* @param argTypes - the story's `argTypes`, as the panel reads them.
|
|
21
|
+
* @param docs - its component's docs, as the plugin served them.
|
|
22
|
+
*/
|
|
23
|
+
export const argControlsOf = (argTypes, docs) => {
|
|
24
|
+
const typed = Object.entries(typedControlsOf(docs)).filter(([name]) => argTypes[name]?.control === undefined && argTypes[name]?.customControl === undefined);
|
|
25
|
+
const named = Object.entries(argTypes).flatMap(([name, argType]) => argType.customControl === undefined ? [] : [[name, argType.customControl]]);
|
|
26
|
+
return Object.fromEntries([...typed, ...named]);
|
|
27
|
+
};
|
|
28
|
+
/** Each story file's docs, loaded once by the frame. */
|
|
29
|
+
const docsLoads = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* A story's component docs, or none when they cannot load: the panel then has no docs either, so no control by type.
|
|
32
|
+
*
|
|
33
|
+
* @param load - the loader.
|
|
34
|
+
*/
|
|
35
|
+
const loadedDocs = (load) => {
|
|
36
|
+
if (load === undefined)
|
|
37
|
+
return Promise.resolve(undefined);
|
|
38
|
+
// a load that failed is tried again by the next values: the docs may be there by then
|
|
39
|
+
const started = docsLoads.get(load) ??
|
|
40
|
+
load().catch(() => {
|
|
41
|
+
docsLoads.delete(load);
|
|
42
|
+
return undefined;
|
|
43
|
+
});
|
|
44
|
+
docsLoads.set(load, started);
|
|
45
|
+
return started;
|
|
46
|
+
};
|
|
47
|
+
/** Whether a module exports a `toStoryValue` function, and a `toPanelValue` function or none. */
|
|
48
|
+
const isFrameModule = (value) => typeof value === "object" &&
|
|
49
|
+
value !== null &&
|
|
50
|
+
"toStoryValue" in value &&
|
|
51
|
+
typeof value.toStoryValue === "function" &&
|
|
52
|
+
(!("toPanelValue" in value) ||
|
|
53
|
+
value.toPanelValue === undefined ||
|
|
54
|
+
typeof value.toPanelValue === "function");
|
|
55
|
+
/** Each frame module's load, kept: every value of a page shares one. */
|
|
56
|
+
const loads = new Map();
|
|
57
|
+
/** The reason an error gives. */
|
|
58
|
+
const reasonOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
59
|
+
/** A frame module, loaded once, or why it cannot be. */
|
|
60
|
+
const loadedModule = async (control, load) => {
|
|
61
|
+
try {
|
|
62
|
+
const started = loads.get(load) ?? load();
|
|
63
|
+
loads.set(load, started);
|
|
64
|
+
const module = await started;
|
|
65
|
+
return isFrameModule(module)
|
|
66
|
+
? module
|
|
67
|
+
: `The control "${control}" names a frame module that exports no toStoryValue function`;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return `The control "${control}" could not load its frame module: ${reasonOf(error)}`;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* One arg's values as its control's frame module turns them.
|
|
75
|
+
*
|
|
76
|
+
* @param module - the module.
|
|
77
|
+
* @param control - the control's name.
|
|
78
|
+
* @param argType - the arg.
|
|
79
|
+
* @param args - the panel's values.
|
|
80
|
+
* @param story - the story.
|
|
81
|
+
*/
|
|
82
|
+
const argOutcome = (module, control, argType, args, story) => {
|
|
83
|
+
const { name } = argType;
|
|
84
|
+
const problems = [];
|
|
85
|
+
let own;
|
|
86
|
+
const ownValue = Object.hasOwn(story.args, name) ? story.args[name] : undefined;
|
|
87
|
+
if (module.toPanelValue !== undefined && ownValue !== undefined) {
|
|
88
|
+
try {
|
|
89
|
+
const named = module.toPanelValue(ownValue, argType);
|
|
90
|
+
own = named !== undefined && isArgValue(named) ? named : undefined;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
problems.push(`The control "${control}" could not name the story's value of "${name}": ${reasonOf(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const named = own === undefined ? {} : { own };
|
|
97
|
+
const given = Object.hasOwn(args, name) ? args[name] : undefined;
|
|
98
|
+
if (given === undefined)
|
|
99
|
+
return { isDropped: false, problems, ...named };
|
|
100
|
+
// the panel's "no value" stays one: the story receives `undefined`, as for any arg
|
|
101
|
+
if (isUnsetArg(given))
|
|
102
|
+
return { value: { made: undefined }, isDropped: false, problems, ...named };
|
|
103
|
+
try {
|
|
104
|
+
return { value: { made: module.toStoryValue(given, argType) }, isDropped: false, problems, ...named };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
problems.push(`The control "${control}" could not turn the value of "${name}": ${reasonOf(error)}`);
|
|
108
|
+
return { isDropped: true, problems, ...named };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The values of the controls with a frame module, as the story and the panel take them.
|
|
113
|
+
*
|
|
114
|
+
* @param args - the panel's values, checked by the shell.
|
|
115
|
+
* @param loaders - the frame modules, by control.
|
|
116
|
+
* @param story - the story's own args, its `argTypes` and its docs.
|
|
117
|
+
*/
|
|
118
|
+
export const controlValuesOf = async (args, loaders, story) => {
|
|
119
|
+
const types = panelArgTypesOf(story.argTypes, []);
|
|
120
|
+
const controls = argControlsOf(types, await loadedDocs(story.loadDocs));
|
|
121
|
+
const outcomes = await Promise.all(Object.entries(controls).flatMap(([name, control]) => {
|
|
122
|
+
const load = Object.hasOwn(loaders, control) ? loaders[control] : undefined;
|
|
123
|
+
if (load === undefined)
|
|
124
|
+
return [];
|
|
125
|
+
const settings = Object.hasOwn(types, name) ? types[name]?.settings : undefined;
|
|
126
|
+
const argType = { name, ...(settings === undefined ? {} : { settings }) };
|
|
127
|
+
return [
|
|
128
|
+
loadedModule(control, load).then((module) => [
|
|
129
|
+
name,
|
|
130
|
+
typeof module === "string"
|
|
131
|
+
? { isDropped: Object.hasOwn(args, name), problems: [module] }
|
|
132
|
+
: argOutcome(module, control, argType, args, story)
|
|
133
|
+
])
|
|
134
|
+
];
|
|
135
|
+
}));
|
|
136
|
+
return {
|
|
137
|
+
values: Object.fromEntries(outcomes.flatMap(([name, outcome]) => (outcome.value === undefined ? [] : [[name, outcome.value.made]]))),
|
|
138
|
+
dropped: outcomes.flatMap(([name, outcome]) => (outcome.isDropped ? [name] : [])),
|
|
139
|
+
own: Object.fromEntries(outcomes.flatMap(([name, outcome]) => (outcome.own === undefined ? [] : [[name, outcome.own]]))),
|
|
140
|
+
// a module that fails is said once, however many args use it
|
|
141
|
+
problems: [...new Set(outcomes.flatMap(([, outcome]) => outcome.problems))]
|
|
142
|
+
};
|
|
143
|
+
};
|
|
@@ -16,6 +16,7 @@ import type { StoryLanguage, StorySource } from "@forgebook/story-format/source"
|
|
|
16
16
|
import { type CatalogueLists } from "../catalogue.ts";
|
|
17
17
|
import { type FramesPlacement } from "../frameOrigins.ts";
|
|
18
18
|
import { type ViewSwitch } from "../switches.ts";
|
|
19
|
+
import { type ControlFrameLoaders } from "./panelControls.ts";
|
|
19
20
|
import type { PlayToolsLoader } from "./playStory.ts";
|
|
20
21
|
import { type FramePlugin } from "./pluginFrames.ts";
|
|
21
22
|
import type { FrameRenderer } from "./renderer.ts";
|
|
@@ -50,6 +51,8 @@ export type FrameConfig = {
|
|
|
50
51
|
readonly plugins?: readonly FramePlugin[];
|
|
51
52
|
/** Where the frames run, so the frame hears only its shell: on the catalogue's own origin by default. */
|
|
52
53
|
readonly frames?: FramesPlacement;
|
|
54
|
+
/** The custom controls' frame modules, by control, each loaded when first needed (the generated frame passes them). */
|
|
55
|
+
readonly argControls?: ControlFrameLoaders;
|
|
53
56
|
};
|
|
54
57
|
/**
|
|
55
58
|
* Shows the story the address names in the given element.
|
|
@@ -34,6 +34,7 @@ import { followTouch } from "./followTouch.js";
|
|
|
34
34
|
import { storyFrameClock } from "./frameClock.js";
|
|
35
35
|
import { ERROR_MESSAGE_CLASS, ERROR_STACK_CLASS, FRAME_STARTED_ATTRIBUTE, NOT_AWAITED_ATTRIBUTE, PENDING_REASON_ATTRIBUTE, STATUS_ATTRIBUTE } from "./frameStatus.js";
|
|
36
36
|
import { applyView, loadSheets, viewSheets } from "./frameView.js";
|
|
37
|
+
import { panelControls } from "./panelControls.js";
|
|
37
38
|
import { panelArgsOf, storyArgsOf } from "./panelValues.js";
|
|
38
39
|
import { startPluginFrames } from "./pluginFrames.js";
|
|
39
40
|
import { shellChannel } from "./shellChannel.js";
|
|
@@ -107,7 +108,7 @@ const failedToLoad = (message, fail) => {
|
|
|
107
108
|
};
|
|
108
109
|
/**
|
|
109
110
|
* Does what a message of the shell asks of the rendered story; the view's live part is applied apart, before the story
|
|
110
|
-
* loads.
|
|
111
|
+
* loads, and so are the panel's values (`panelControls.ts`).
|
|
111
112
|
*
|
|
112
113
|
* @param message - the message.
|
|
113
114
|
* @param controls - the rendered story's controls.
|
|
@@ -121,8 +122,6 @@ const failedToLoad = (message, fail) => {
|
|
|
121
122
|
const answerShell = (message, controls, storyId, pendingReason, rules, axeUrl, tellShell) => {
|
|
122
123
|
if (message.type === "forgebook:replay")
|
|
123
124
|
void controls.replay();
|
|
124
|
-
else if (message.type === "forgebook:args")
|
|
125
|
-
controls.setPanelArgs(message.args);
|
|
126
125
|
else if (message.type === "forgebook:events")
|
|
127
126
|
controls.setEvents(message.names);
|
|
128
127
|
else if (message.type === "forgebook:matrix")
|
|
@@ -327,10 +326,9 @@ export const mountFrame = async (container, config) => {
|
|
|
327
326
|
// why the story was not seen in its final state: a check of what it shows would pass on a fallback
|
|
328
327
|
let pendingReason;
|
|
329
328
|
const clock = storyFrameClock(story.clockStart, isClockHeldIn(window.location.search));
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
});
|
|
329
|
+
// the controls with a frame module, which turn the panel's values first; their docs name those given by type
|
|
330
|
+
const typed = panelControls(config.argControls, source, location.story.file, reportError);
|
|
331
|
+
shell.listen((message) => typed.take(message, { story, controls, tellReady }, (drawn) => answerShell(message, drawn, storyId, pendingReason, catalogue.a11y, config.axeUrl, tellShell)));
|
|
334
332
|
// the language's texts, and the switches the story reads (the language's provider reads its id among them); a
|
|
335
333
|
// language the source gives as a loader is loaded as it is shown
|
|
336
334
|
const shownFor = async (live) => {
|
|
@@ -353,7 +351,7 @@ export const mountFrame = async (container, config) => {
|
|
|
353
351
|
storyId,
|
|
354
352
|
canReplay: story.replay !== undefined,
|
|
355
353
|
hasClock: story.clockStart !== undefined,
|
|
356
|
-
args,
|
|
354
|
+
args: typed.withOwn(args),
|
|
357
355
|
textKeys: storyTextKeysOf(story.args),
|
|
358
356
|
argTypes: panelArgTypesOf(story.argTypes, excludedControls(story.parameters["controls"])),
|
|
359
357
|
controls: story.controls,
|
|
@@ -373,7 +371,7 @@ export const mountFrame = async (container, config) => {
|
|
|
373
371
|
// the first of the list is the innermost
|
|
374
372
|
decorators: [...preview.decorators, ...plugins.decorators],
|
|
375
373
|
...shown,
|
|
376
|
-
resolveArgs: (args) => storyArgsOf(args, source.namedValues),
|
|
374
|
+
resolveArgs: (args) => typed.storyArgs(storyArgsOf(args, source.namedValues)),
|
|
377
375
|
onControlsReady: (next, outcome) => {
|
|
378
376
|
controls = next;
|
|
379
377
|
clearRetry(sessionStorageOf(window), window.location.href);
|
|
@@ -421,7 +419,7 @@ export const mountFrame = async (container, config) => {
|
|
|
421
419
|
values: values.map(previewValue),
|
|
422
420
|
details: values.map(detailOf)
|
|
423
421
|
}),
|
|
424
|
-
panelArgsOf: (values) => panelArgsOf(values, source.namedValues),
|
|
422
|
+
panelArgsOf: (values) => typed.panelArgs(values, panelArgsOf(values, source.namedValues)),
|
|
425
423
|
onArgsChange: (args) => tellShell({ type: "forgebook:story-args", storyId, args })
|
|
426
424
|
});
|
|
427
425
|
// an edit of the story file, or of a file it re-exports stories from, draws the story anew in place: the reader keeps
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a frame keeps of the custom controls with a frame module: the panel's latest values as their modules turned them,
|
|
3
|
+
* and the story's own values they named. The turning itself (`controlValues.ts`) loads when the shell first gives values.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Turning values takes a load, so the shell's other messages wait for the values before them: a Replay or a matrix
|
|
7
|
+
* right after a change draws the changed values, as without any frame module.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { StorySource } from "@forgebook/story-format/source";
|
|
12
|
+
import type { PanelArgs, ShellMessage } from "../messages.ts";
|
|
13
|
+
import type { ControlStory } from "./controlValues.ts";
|
|
14
|
+
import type { StoryControls } from "./storyRun.ts";
|
|
15
|
+
/** The custom controls' frame modules, by control: how to load each. */
|
|
16
|
+
export type ControlFrameLoaders = Readonly<Record<string, () => Promise<unknown>>>;
|
|
17
|
+
/** The story a message is for, as `take` reads it. */
|
|
18
|
+
export type TakenStory<TControls> = {
|
|
19
|
+
/** The story as it is now. */
|
|
20
|
+
readonly story: Omit<ControlStory, "loadDocs">;
|
|
21
|
+
/** Its controls once drawn; none before, when nothing is answered. */
|
|
22
|
+
readonly controls: TControls | undefined;
|
|
23
|
+
/** Tells the shell the story's own values again. */
|
|
24
|
+
readonly tellReady: () => void;
|
|
25
|
+
};
|
|
26
|
+
/** A frame's custom controls with a frame module; with none, it hands the shell's messages on as they come. */
|
|
27
|
+
export type PanelControls = {
|
|
28
|
+
/**
|
|
29
|
+
* Answers a message of the shell: the panel's values turned first, then given to the story; any other message after
|
|
30
|
+
* the values before it. A later change of values overtakes one still turning.
|
|
31
|
+
*
|
|
32
|
+
* @param message - the message.
|
|
33
|
+
* @param story - the story as it is now, and its controls once drawn; none before, when nothing is answered.
|
|
34
|
+
* @param answer - answers any other message.
|
|
35
|
+
*/
|
|
36
|
+
readonly take: <TControls extends Pick<StoryControls, "setPanelArgs">>(message: ShellMessage, story: TakenStory<TControls>, answer: (controls: TControls) => void) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The story's values with the turned ones over them; an arg whose control failed is left out, so the story keeps its
|
|
39
|
+
* own: its JSON is not what the story takes.
|
|
40
|
+
*
|
|
41
|
+
* @param resolved - the panel's values as the story would take them without the modules.
|
|
42
|
+
*/
|
|
43
|
+
readonly storyArgs: (resolved: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
|
|
44
|
+
/**
|
|
45
|
+
* The story's own values for the panel, with those the modules named over them.
|
|
46
|
+
*
|
|
47
|
+
* @param own - the story's own values, as plain data gives them.
|
|
48
|
+
*/
|
|
49
|
+
readonly withOwn: (own: PanelArgs) => PanelArgs;
|
|
50
|
+
/**
|
|
51
|
+
* The panel's values of a story that changed its own args: a value a module made is given back as the panel's JSON it
|
|
52
|
+
* came from, which plain data cannot hold, so the panel keeps the reader's choice.
|
|
53
|
+
*
|
|
54
|
+
* @param values - the story's values.
|
|
55
|
+
* @param shown - the same as plain data gives them.
|
|
56
|
+
*/
|
|
57
|
+
readonly panelArgs: (values: Readonly<Record<string, unknown>>, shown: PanelArgs) => PanelArgs;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A frame's custom controls with a frame module.
|
|
61
|
+
*
|
|
62
|
+
* @param loaders - the frame modules, by control; none hands every message on as it comes.
|
|
63
|
+
* @param source - the story's source, whose docs name the controls given by type.
|
|
64
|
+
* @param file - the story's file, the key of its docs.
|
|
65
|
+
* @param report - says what went wrong.
|
|
66
|
+
*/
|
|
67
|
+
export declare const panelControls: (loaders: ControlFrameLoaders | undefined, source: StorySource, file: string, report: (message: string) => void) => PanelControls;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { sourceFiles } from "../sourceFiles.js";
|
|
2
|
+
import { panelArgTypesOf } from "../storyArgTypes.js";
|
|
3
|
+
import { STATUS_ATTRIBUTE } from "./frameStatus.js";
|
|
4
|
+
import { panelArgsOf } from "./panelValues.js";
|
|
5
|
+
/**
|
|
6
|
+
* Whether a frame module may have something to do for the story, so its docs are worth loading: the panel gives a
|
|
7
|
+
* value (any prop may be one a control takes by type), the story's `argTypes` name a control with a frame module, or
|
|
8
|
+
* the story gives itself a value plain data cannot show (an icon a module could name). A story with none of these, most
|
|
9
|
+
* of a page's frames, loads neither the turning code nor its docs.
|
|
10
|
+
*
|
|
11
|
+
* @param args - the panel's values.
|
|
12
|
+
* @param story - the story.
|
|
13
|
+
* @param loaders - the frame modules, by control.
|
|
14
|
+
* @param source - the story's source, whose named values show some values plainly.
|
|
15
|
+
*/
|
|
16
|
+
const mayTurn = (args, story, loaders, source) => {
|
|
17
|
+
if (Object.keys(args).length > 0)
|
|
18
|
+
return true;
|
|
19
|
+
const named = Object.values(panelArgTypesOf(story.argTypes, [])).some((argType) => argType.customControl !== undefined && Object.hasOwn(loaders, argType.customControl));
|
|
20
|
+
const shown = panelArgsOf(story.args, source.namedValues);
|
|
21
|
+
// an event's handler is no value a control sets: a type's control never takes an event
|
|
22
|
+
return (named ||
|
|
23
|
+
Object.entries(story.args).some(([name, value]) => value !== undefined && !Object.hasOwn(shown, name) && !/^on[A-Z]/.test(name)));
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A frame's custom controls with a frame module.
|
|
27
|
+
*
|
|
28
|
+
* @param loaders - the frame modules, by control; none hands every message on as it comes.
|
|
29
|
+
* @param source - the story's source, whose docs name the controls given by type.
|
|
30
|
+
* @param file - the story's file, the key of its docs.
|
|
31
|
+
* @param report - says what went wrong.
|
|
32
|
+
*/
|
|
33
|
+
export const panelControls = (loaders, source, file, report) => {
|
|
34
|
+
const loadDocs = sourceFiles(source).docs[file];
|
|
35
|
+
let made;
|
|
36
|
+
// the panel's values last given, as JSON: what the made values came from
|
|
37
|
+
let given = {};
|
|
38
|
+
let count = 0;
|
|
39
|
+
// the values still turning, which the other messages wait for
|
|
40
|
+
let turning = Promise.resolve();
|
|
41
|
+
const reportError = (error) => report(error instanceof Error ? error.message : String(error));
|
|
42
|
+
return {
|
|
43
|
+
take: (message, { story, controls, tellReady }, answer) => {
|
|
44
|
+
if (controls === undefined)
|
|
45
|
+
return;
|
|
46
|
+
if (message.type !== "forgebook:args") {
|
|
47
|
+
if (loaders === undefined)
|
|
48
|
+
answer(controls);
|
|
49
|
+
else
|
|
50
|
+
turning = turning.then(() => answer(controls));
|
|
51
|
+
}
|
|
52
|
+
else if (loaders === undefined)
|
|
53
|
+
controls.setPanelArgs(message.args);
|
|
54
|
+
else if (!mayTurn(message.args, story, loaders, source)) {
|
|
55
|
+
// nothing a module could turn or name: no module, and no docs, load for this story
|
|
56
|
+
const hadOwn = Object.keys(made?.own ?? {}).length > 0;
|
|
57
|
+
count += 1;
|
|
58
|
+
made = undefined;
|
|
59
|
+
turning = turning.then(() => {
|
|
60
|
+
controls.setPanelArgs(message.args);
|
|
61
|
+
if (hadOwn)
|
|
62
|
+
tellReady();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
count += 1;
|
|
67
|
+
const asked = count;
|
|
68
|
+
turning = import("./controlValues.js")
|
|
69
|
+
.then(({ controlValuesOf }) => controlValuesOf(message.args, loaders, { ...story, loadDocs }))
|
|
70
|
+
.then((next) => {
|
|
71
|
+
// values the reader changed again meanwhile are the next call's to draw
|
|
72
|
+
if (asked !== count)
|
|
73
|
+
return;
|
|
74
|
+
const isOwnNew = JSON.stringify(next.own) !== JSON.stringify(made?.own ?? {});
|
|
75
|
+
made = next;
|
|
76
|
+
given = message.args;
|
|
77
|
+
controls.setPanelArgs(message.args);
|
|
78
|
+
for (const problem of next.problems)
|
|
79
|
+
report(problem);
|
|
80
|
+
// the panel shows the story's own value a module names, told once known; a story in error stays one
|
|
81
|
+
const isError = document.documentElement.getAttribute(STATUS_ATTRIBUTE) === "error";
|
|
82
|
+
if (isOwnNew && !isError)
|
|
83
|
+
tellReady();
|
|
84
|
+
})
|
|
85
|
+
.catch((error) => {
|
|
86
|
+
reportError(error);
|
|
87
|
+
// without the modules the story still takes the values plain data can give
|
|
88
|
+
if (asked !== count)
|
|
89
|
+
return;
|
|
90
|
+
made = undefined;
|
|
91
|
+
controls.setPanelArgs(message.args);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
storyArgs: (resolved) => {
|
|
96
|
+
const dropped = made?.dropped ?? [];
|
|
97
|
+
return Object.fromEntries(Object.entries({ ...resolved, ...made?.values }).filter(([name]) => !dropped.includes(name)));
|
|
98
|
+
},
|
|
99
|
+
withOwn: (own) => ({ ...own, ...made?.own }),
|
|
100
|
+
panelArgs: (values, shown) => {
|
|
101
|
+
const kept = Object.entries(made?.values ?? {}).flatMap(([name, value]) => {
|
|
102
|
+
const json = given[name];
|
|
103
|
+
return json !== undefined && Object.hasOwn(values, name) && values[name] === value
|
|
104
|
+
? [[name, json]]
|
|
105
|
+
: [];
|
|
106
|
+
});
|
|
107
|
+
return { ...shown, ...Object.fromEntries(kept) };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
};
|
|
@@ -20,6 +20,7 @@ import { findStory } from "../storyTree.js";
|
|
|
20
20
|
import { CodeText } from "./CodeText.js";
|
|
21
21
|
import { useCatalogue } from "./catalogueContext.js";
|
|
22
22
|
import { blockPlaces, catalogueValues, DocBlocks, queuedBlocks } from "./DocBlocks.js";
|
|
23
|
+
import { usePageWidth } from "./PageWidth.js";
|
|
23
24
|
import { PAGE_STRINGS } from "./pageStrings.js";
|
|
24
25
|
import { useLoaded } from "./useLoaded.js";
|
|
25
26
|
import { usePageFrames } from "./usePageFrames.js";
|
|
@@ -53,6 +54,7 @@ export const AuthoredHome = ({ load, title, plugins, tree, sources, recent, href
|
|
|
53
54
|
const places = useMemo(() => blockPlaces(content.blocks), [content.blocks]);
|
|
54
55
|
const values = useMemo(() => catalogueValues(title, tree), [title, tree]);
|
|
55
56
|
const frames = usePageFrames({ stories, queued, pageRef, view, touchOverrides });
|
|
57
|
+
const pageWidth = usePageWidth();
|
|
56
58
|
useScrollMemory(pageRef, scroll);
|
|
57
59
|
const openLink = useCallback((href) => {
|
|
58
60
|
if (href.startsWith("#")) {
|
|
@@ -84,7 +86,7 @@ export const AuthoredHome = ({ load, title, plugins, tree, sources, recent, href
|
|
|
84
86
|
plugins,
|
|
85
87
|
strings: catalogue.strings
|
|
86
88
|
};
|
|
87
|
-
return (_jsxs("div", { className: "forgebook-workspace", children: [header,
|
|
89
|
+
return (_jsxs("div", { className: "forgebook-workspace", children: [header, _jsxs("div", { ref: pageRef, className: "forgebook-scroll", children: [_jsx("div", { className: "forgebook-home__tools", children: pageWidth.toggle }), _jsxs("div", { ref: bodyRef, className: "forgebook-home forgebook-home--written", "data-page-width": pageWidth.width, children: [loaded.status === "loading" ? null : (_jsxs("header", { className: "forgebook-home__head", children: [_jsx("h1", { className: "forgebook-home__title", children: filledText(content.title ?? title, values) }), content.description === undefined ? null : (_jsx("p", { className: "forgebook-doc__lead", children: _jsx(CodeText, { text: filledText(content.description, values) }) }))] })), loaded.status === "error" ? (_jsxs("p", { className: "forgebook-problems", role: "alert", children: [PAGE_STRINGS.homeError, ": ", loaded.message] })) : null, _jsx(HomeNotes, { notes: content.notes }), _jsx(DocBlocks, { blocks: content.blocks, context: context })] })] })] }));
|
|
88
90
|
};
|
|
89
91
|
/**
|
|
90
92
|
* The written home as the shell draws it, its loader bound: what the page's entry hands the shell, so the shell's own
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The panel's Code tab: the story's `render` as written in its file, or, for a story drawn from its args, the
|
|
3
3
|
* component's JSX with the args its file writes as code and the values of the moment (the story's and the Props
|
|
4
|
-
* tab's), with a button that copies it.
|
|
4
|
+
* tab's), with a button that copies it. A value a custom control sets is written as the code its control's `toCode`
|
|
5
|
+
* gives (`icon={IconStar}`), its module loaded as the panel loads it.
|
|
5
6
|
*
|
|
6
7
|
* @module
|
|
7
8
|
*/
|
|
8
|
-
import type
|
|
9
|
+
import { type ReactNode } from "react";
|
|
9
10
|
import { type StoryCodeSource } from "../storyCodeText.ts";
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
import type { DockTabsInput } from "./dockTabs.tsx";
|
|
12
|
+
/** The tab's props: the story whose code it shows, and what the panel knows of its props. */
|
|
13
|
+
export type CodeTabProps = StoryCodeSource & {
|
|
14
|
+
/** The component's docs, the story's `argTypes` and the catalogue's custom controls; none writes every value as JSON. */
|
|
15
|
+
readonly panel?: Pick<DockTabsInput, "docs" | "argTypes" | "storyArgs" | "controls" | "argControls">;
|
|
16
|
+
};
|
|
12
17
|
/**
|
|
13
18
|
* The Code tab.
|
|
14
19
|
*
|
|
15
20
|
* @param props - the file's code, the story, the component's name and the args.
|
|
16
21
|
*/
|
|
17
|
-
export declare const CodeTab: (
|
|
22
|
+
export declare const CodeTab: (tab: CodeTabProps) => ReactNode;
|
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The panel's Code tab: the story's `render` as written in its file, or, for a story drawn from its args, the
|
|
4
|
+
* component's JSX with the args its file writes as code and the values of the moment (the story's and the Props
|
|
5
|
+
* tab's), with a button that copies it. A value a custom control sets is written as the code its control's `toCode`
|
|
6
|
+
* gives (`icon={IconStar}`), its module loaded as the panel loads it.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useMemo, useState } from "react";
|
|
11
|
+
import { panelProps, settableProps } from "../argTypes.js";
|
|
12
|
+
import { controlCodeOf } from "../controlCode.js";
|
|
2
13
|
import { storyCodeText } from "../storyCodeText.js";
|
|
3
14
|
import { useCodeWriter } from "./codeWriter.js";
|
|
4
15
|
import { LATER_STRINGS } from "./laterStrings.js";
|
|
5
16
|
import { hasRenderCode, StoryCodeBlock } from "./StoryCodeBlock.js";
|
|
6
17
|
import { STRINGS } from "./strings.js";
|
|
7
18
|
import { useCopy } from "./useCopy.js";
|
|
19
|
+
/**
|
|
20
|
+
* The code the story's custom controls write its values as, once their modules are loaded.
|
|
21
|
+
*
|
|
22
|
+
* @param props - the story and what the panel knows of its props.
|
|
23
|
+
*/
|
|
24
|
+
const useArgCode = ({ panel, args }) => {
|
|
25
|
+
const props = useMemo(() => panel === undefined || panel.docs.status !== "ready"
|
|
26
|
+
? []
|
|
27
|
+
: settableProps(panelProps(panel.docs.value.props, panel.argTypes, panel.storyArgs), panel.controls, {}), [panel]);
|
|
28
|
+
const names = [...new Set(props.flatMap((prop) => prop.customControl ?? []))].join("\n");
|
|
29
|
+
const [coders, setCoders] = useState({});
|
|
30
|
+
const loaders = panel?.argControls;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (names === "" || loaders === undefined)
|
|
33
|
+
return;
|
|
34
|
+
let isCurrent = true;
|
|
35
|
+
void import("./customControls.js")
|
|
36
|
+
.then(({ loadControl }) => Promise.all(names.split("\n").map(async (name) => [name, await loadControl(name, loaders)])))
|
|
37
|
+
.then((loaded) => {
|
|
38
|
+
if (!isCurrent)
|
|
39
|
+
return;
|
|
40
|
+
setCoders(Object.fromEntries(loaded.flatMap(([name, control]) => control.status === "ready" ? [[name, control.module.toCode]] : [])));
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
isCurrent = false;
|
|
44
|
+
};
|
|
45
|
+
}, [names, loaders]);
|
|
46
|
+
return useMemo(() => controlCodeOf(props, args, coders), [props, args, coders]);
|
|
47
|
+
};
|
|
8
48
|
/**
|
|
9
49
|
* The Code tab.
|
|
10
50
|
*
|
|
11
51
|
* @param props - the file's code, the story, the component's name and the args.
|
|
12
52
|
*/
|
|
13
|
-
export const CodeTab = (
|
|
53
|
+
export const CodeTab = (tab) => {
|
|
14
54
|
const { isCopied, copy } = useCopy();
|
|
15
55
|
const write = useCodeWriter();
|
|
56
|
+
const argCode = useArgCode(tab);
|
|
57
|
+
const { panel: _panel, ...source } = tab;
|
|
58
|
+
const props = { ...source, argCode };
|
|
16
59
|
const hasRender = hasRenderCode(props);
|
|
17
60
|
return (_jsxs("div", { className: "forgebook-prop-group", children: [_jsxs("div", { className: "forgebook-calls__head", children: [_jsx("h3", { className: "forgebook-prop-group__title", children: hasRender ? LATER_STRINGS.codeRender : LATER_STRINGS.codeFromArgs }), _jsx("button", { type: "button", className: "forgebook-text-button", onClick: () => copy(storyCodeText(props, write)), "aria-live": "polite", children: isCopied ? STRINGS.copied : LATER_STRINGS.copy })] }), _jsx(StoryCodeBlock, { ...props })] }));
|
|
18
61
|
};
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* example.
|
|
5
5
|
*
|
|
6
6
|
* @remarks
|
|
7
|
-
* The frames load a few at a time (`useFrameQueue`),
|
|
8
|
-
*
|
|
7
|
+
* The frames load a few at a time (`useFrameQueue`), each as it comes near the visible part of the page, the nearest
|
|
8
|
+
* first and the demo first among those in view: a component may have a hundred stories. The view's live part reaches every frame as a message, as
|
|
9
9
|
* on the story page; the props panel and Replay belong to a story's own page, which each story's name links to. The
|
|
10
10
|
* documentation file's HTML is the package's own, made at build time (`vite/componentPage.ts`). Who reads the page
|
|
11
11
|
* reorders and filters what goes around the frames (`audienceLayout.ts`); what only one audience wants loads on first use,
|
|
@@ -51,8 +51,8 @@ export type ComponentPageProps = {
|
|
|
51
51
|
readonly heightOf: (storyId: string) => number | undefined;
|
|
52
52
|
/** The colour the stories' page has in the view (their source's), which each box waits on; none without one. */
|
|
53
53
|
readonly background: string | undefined;
|
|
54
|
-
/** What stays above the page while it scrolls: the component's header. */
|
|
55
|
-
readonly
|
|
54
|
+
/** What stays above the page while it scrolls: the component's header, with the page's tools beside its picker. */
|
|
55
|
+
readonly headerWith: (tools: ReactNode) => ReactNode;
|
|
56
56
|
/** Where its scrolling part starts: the shell's visit of the page and every page's last position. */
|
|
57
57
|
readonly scroll: PageScroll;
|
|
58
58
|
/** What "Used in" reads and opens; none when the catalogue turned it off. */
|
|
@@ -97,4 +97,4 @@ export declare const PageApi: ({ docs, isLogic, componentGroups, requiredFirst }
|
|
|
97
97
|
*
|
|
98
98
|
* @param props - the component, the view, its loaders and how to open a story.
|
|
99
99
|
*/
|
|
100
|
-
export declare const ComponentPage: ({ component, anchor, view, touchOverrides, onTouchChange, frameUrl, hrefOf, onOpen, loadDocs, loadCode, loadPage, heightOf, background,
|
|
100
|
+
export declare const ComponentPage: ({ component, anchor, view, touchOverrides, onTouchChange, frameUrl, hrefOf, onOpen, loadDocs, loadCode, loadPage, heightOf, background, headerWith, scroll, usedIn, onViewChange, tree, componentId, pageHrefOf, onOpenPage, loadTokens }: ComponentPageProps) => ReactNode;
|
|
@@ -14,6 +14,7 @@ import { Example } from "./Example.js";
|
|
|
14
14
|
import { LATER_STRINGS } from "./laterStrings.js";
|
|
15
15
|
import { LazyView, lazyModule } from "./lazyModule.js";
|
|
16
16
|
import { PackageHtml } from "./PackageHtml.js";
|
|
17
|
+
import { usePageWidth } from "./PageWidth.js";
|
|
17
18
|
import { PageTagView } from "./pageTagView.js";
|
|
18
19
|
import { UsedIn } from "./UsedIn.js";
|
|
19
20
|
import { useComponentDocs } from "./useComponentDocs.js";
|
|
@@ -91,11 +92,12 @@ export const PageApi = ({ docs, isLogic, componentGroups, requiredFirst }) => do
|
|
|
91
92
|
*
|
|
92
93
|
* @param props - the component, the view, its loaders and how to open a story.
|
|
93
94
|
*/
|
|
94
|
-
export const ComponentPage = ({ component, anchor, view, touchOverrides, onTouchChange, frameUrl, hrefOf, onOpen, loadDocs, loadCode, loadPage, heightOf, background,
|
|
95
|
+
export const ComponentPage = ({ component, anchor, view, touchOverrides, onTouchChange, frameUrl, hrefOf, onOpen, loadDocs, loadCode, loadPage, heightOf, background, headerWith, scroll, usedIn, onViewChange, tree, componentId, pageHrefOf, onOpenPage, loadTokens }) => {
|
|
95
96
|
const pageRef = useRef(null);
|
|
96
97
|
const docs = useComponentDocs(loadDocs);
|
|
97
98
|
const code = useStoryCode(loadCode);
|
|
98
99
|
const page = usePageDoc(loadPage);
|
|
100
|
+
const pageWidth = usePageWidth();
|
|
99
101
|
const demo = mainStory(component);
|
|
100
102
|
const examples = useMemo(() => component.stories.filter((story) => story.id !== demo?.id), [component, demo]);
|
|
101
103
|
const catalogue = useCatalogue();
|
|
@@ -163,5 +165,5 @@ export const ComponentPage = ({ component, anchor, view, touchOverrides, onTouch
|
|
|
163
165
|
usedIn,
|
|
164
166
|
pageRef
|
|
165
167
|
};
|
|
166
|
-
return (_jsxs("div", { className: "forgebook-workspace", children: [
|
|
168
|
+
return (_jsxs("div", { className: "forgebook-workspace", children: [headerWith(pageWidth.toggle), anchor === undefined ? null : (_jsx(Suspense, { fallback: null, children: _jsx(AnchorScroll, { pageRef: pageRef, anchor: anchor, visit: scroll.visit, isReady: docs.status !== "loading" && page.status !== "loading" }) })), _jsx("div", { ref: pageRef, className: "forgebook-scroll", children: _jsxs("div", { className: "forgebook-doc", "data-audience": view.audience === "everyone" ? undefined : view.audience, "data-page-width": pageWidth.width, children: [hasTags ? _jsx(PageTagView, { place: "page", component: component, loadDocs: loadDocs }) : null, _jsx(AudienceSlot, { place: "beforeDemo", audience: audienceProps, children: _jsx(Lead, { page: page, docs: componentDocs }) }), demo === undefined ? null : (_jsx(Example, { ...context, story: demo, isDemo: true, isMounted: queue.mounted.has(demo.id), observe: queue.observerOf(demo.id) })), _jsxs(AudienceSlot, { place: "afterDemo", audience: audienceProps, children: [_jsx(Prose, { page: page }), _jsx(PageApi, { docs: componentDocs, isLogic: isLogic, componentGroups: component.propGroups, requiredFirst: component.requiredFirst })] }), examples.length === 0 ? null : (_jsxs("section", { className: "forgebook-doc__section", "aria-labelledby": "forgebook-examples", children: [_jsx("h2", { id: "forgebook-examples", children: LATER_STRINGS.examples }), groupedStories(examples, catalogue.storyGroups.map((group) => group.id)).map((section) => (_jsxs("section", { className: "forgebook-doc__group", children: [section.group === undefined ? null : (_jsx("h3", { className: "forgebook-doc__group-name", children: groupLabel(catalogue.storyGroups, section.group) })), section.stories.map((story) => (_jsx(Example, { ...context, story: story, isDemo: false, titleTag: section.group === undefined ? "h3" : "h4", isMounted: queue.mounted.has(story.id), observe: queue.observerOf(story.id) }, story.id)))] }, section.group ?? "")))] })), _jsx(AudienceSlot, { place: "afterExamples", audience: audienceProps, children: usedIn === undefined ? null : _jsx(UsedIn, { title: component.title, ...usedIn }) })] }, `${component.file}|${component.title}`) })] }));
|
|
167
169
|
};
|