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.
Files changed (70) hide show
  1. package/README.md +83 -3
  2. package/dist/bin/forgebook/agentText.js +7 -0
  3. package/dist/bin/forgebook/guide/commands.md +5 -0
  4. package/dist/bin/forgebook/guide/stories.md +7 -2
  5. package/dist/bin/forgebook/verifyBase.js +10 -2
  6. package/dist/bin/forgebook/verifyVerdict.js +27 -1
  7. package/dist/src/argControl.d.ts +51 -3
  8. package/dist/src/argTypes.js +4 -2
  9. package/dist/src/componentDocs.d.ts +12 -0
  10. package/dist/src/componentDocs.js +5 -1
  11. package/dist/src/controlCode.d.ts +27 -0
  12. package/dist/src/controlCode.js +41 -0
  13. package/dist/src/frame/controlValues.d.ts +39 -0
  14. package/dist/src/frame/controlValues.js +143 -0
  15. package/dist/src/frame/mountFrame.d.ts +3 -0
  16. package/dist/src/frame/mountFrame.js +8 -10
  17. package/dist/src/frame/panelControls.d.ts +67 -0
  18. package/dist/src/frame/panelControls.js +110 -0
  19. package/dist/src/shell/AuthoredHome.js +3 -1
  20. package/dist/src/shell/CodeTab.d.ts +10 -5
  21. package/dist/src/shell/CodeTab.js +44 -1
  22. package/dist/src/shell/ComponentPage.d.ts +5 -5
  23. package/dist/src/shell/ComponentPage.js +4 -2
  24. package/dist/src/shell/CustomControl.js +14 -21
  25. package/dist/src/shell/HomePage.js +5 -3
  26. package/dist/src/shell/PageWidth.d.ts +29 -0
  27. package/dist/src/shell/PageWidth.js +18 -0
  28. package/dist/src/shell/PropRows.d.ts +2 -12
  29. package/dist/src/shell/PropRows.js +6 -7
  30. package/dist/src/shell/PropsTab.js +2 -3
  31. package/dist/src/shell/Shell.js +5 -2
  32. package/dist/src/shell/StoryPicker.d.ts +4 -2
  33. package/dist/src/shell/StoryPicker.js +3 -3
  34. package/dist/src/shell/Switch.d.ts +29 -0
  35. package/dist/src/shell/Switch.js +11 -0
  36. package/dist/src/shell/customControls.d.ts +7 -0
  37. package/dist/src/shell/customControls.js +12 -1
  38. package/dist/src/shell/dockTabs.js +2 -1
  39. package/dist/src/shell/frameOrder.d.ts +35 -0
  40. package/dist/src/shell/frameOrder.js +45 -0
  41. package/dist/src/shell/laterStrings.d.ts +1 -0
  42. package/dist/src/shell/laterStrings.js +1 -0
  43. package/dist/src/shell/shell.css +36 -9
  44. package/dist/src/shell/useFrameQueue.d.ts +8 -3
  45. package/dist/src/shell/useFrameQueue.js +42 -12
  46. package/dist/src/shell/useScrollSettled.d.ts +18 -0
  47. package/dist/src/shell/useScrollSettled.js +56 -0
  48. package/dist/src/shell/valueEditors/ValueField.js +2 -1
  49. package/dist/src/storyCheck.d.ts +14 -0
  50. package/dist/src/storyCheck.js +16 -0
  51. package/dist/src/storyCodeText.d.ts +8 -2
  52. package/dist/src/storyCodeText.js +8 -5
  53. package/dist/src/storyControls.d.ts +3 -0
  54. package/dist/src/vite/agent.types.d.ts +8 -1
  55. package/dist/src/vite/agentRoutes.js +38 -10
  56. package/dist/src/vite/agentStoryCode.d.ts +3 -0
  57. package/dist/src/vite/agentStoryCode.js +8 -2
  58. package/dist/src/vite/argControlsConfig.d.ts +30 -0
  59. package/dist/src/vite/argControlsConfig.js +176 -0
  60. package/dist/src/vite/catalogueConfig.js +6 -1
  61. package/dist/src/vite/config.d.ts +38 -3
  62. package/dist/src/vite/configFile.d.ts +3 -2
  63. package/dist/src/vite/configFile.js +6 -21
  64. package/dist/src/vite/configKeys.js +2 -1
  65. package/dist/src/vite/forgebookPlugin.js +3 -1
  66. package/dist/src/vite/pages.js +12 -1
  67. package/dist/src/vite/propDocs.js +58 -3
  68. package/dist/src/vite/typedControls.d.ts +31 -0
  69. package/dist/src/vite/typedControls.js +66 -0
  70. 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
- shell.listen((message) => {
331
- if (controls !== undefined)
332
- answerShell(message, controls, storyId, pendingReason, catalogue.a11y, config.axeUrl, tellShell);
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, _jsx("div", { ref: pageRef, className: "forgebook-scroll", children: _jsxs("div", { ref: bodyRef, className: "forgebook-home forgebook-home--written", 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 })] }) })] }));
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 { ReactNode } from "react";
9
+ import { type ReactNode } from "react";
9
10
  import { type StoryCodeSource } from "../storyCodeText.ts";
10
- /** The tab's props: the story whose code it shows. */
11
- export type CodeTabProps = StoryCodeSource;
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: (props: CodeTabProps) => ReactNode;
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 = (props) => {
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`), in the page's order, the demo first, each as it comes near the
8
- * visible part of the page: a component may have twenty stories. The view's live part reaches every frame as a message, as
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 header: ReactNode;
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, header, scroll, usedIn, onViewChange, tree, componentId, pageHrefOf, onOpenPage, loadTokens }: ComponentPageProps) => ReactNode;
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, header, scroll, usedIn, onViewChange, tree, componentId, pageHrefOf, onOpenPage, loadTokens }) => {
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: [header, 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, 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}`) })] }));
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
  };