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
package/README.md
CHANGED
|
@@ -252,6 +252,9 @@ too, with a warning.
|
|
|
252
252
|
them and hides the rest in the View menu; their browser keeps it ([below](#the-toolbar)).
|
|
253
253
|
- **A light and a dark catalogue**: the reader picks Light, Dark or System in the Interface menu at the sidebar's foot
|
|
254
254
|
(their browser keeps it); `appearance` in the configuration sets the default. The stories keep their own theme, from the view's switches.
|
|
255
|
+
- **Contained or full width**: the button beside the Stories picker, and at the top of the home, sets the home and a
|
|
256
|
+
component's page in a centred column (pressed, the default) or across the whole width; their browser keeps it. A
|
|
257
|
+
docs page keeps its reading column, and a story's page its own width controls.
|
|
255
258
|
- **Keys and named views**: `?` lists the keys, as does the menu at the sidebar's foot, where a reader can turn off every
|
|
256
259
|
key without a modifier for this browser; a key pressed inside a story, in a text field or in a custom element stays
|
|
257
260
|
theirs. A key cycles a control's values (`t` the theme, Shift+`t` back); the catalogue sets the
|
|
@@ -727,8 +730,9 @@ The editors are one chunk that loads when the first such prop is drawn: a catalo
|
|
|
727
730
|
|
|
728
731
|
### Controls of your own
|
|
729
732
|
|
|
730
|
-
A control the panel lacks is a module of yours, registered by name and loaded when a story names it
|
|
731
|
-
|
|
733
|
+
A control the panel lacks is a module of yours, registered by name and loaded when a story names it, or a prop it
|
|
734
|
+
matches by type is shown ([below](#controls-by-type-and-from-a-librarys-preset)). A catalogue that registers none loads
|
|
735
|
+
nothing for this.
|
|
732
736
|
|
|
733
737
|
```ts
|
|
734
738
|
// forgebook.config.ts
|
|
@@ -752,7 +756,7 @@ export const mount = (element: HTMLElement, props: ArgControlProps) => {
|
|
|
752
756
|
};
|
|
753
757
|
```
|
|
754
758
|
|
|
755
|
-
`mount(element, { value, onChange, argType })` draws in the element and returns what takes it away. It is a function rather
|
|
759
|
+
`mount(element, { value, onChange, argType, strings })` draws in the element and returns what takes it away. It is a function rather
|
|
756
760
|
than a component, so it can be written in any framework, whatever the stories' is: `createApp` in Vue, `mount` in Svelte,
|
|
757
761
|
plain DOM. `argType` holds the arg's `name`, `label` and `description`, and the control's `options`, `min`, `max`, `step`
|
|
758
762
|
and `settings`. The control keeps what it draws: it is mounted again only when the value changes from outside it.
|
|
@@ -775,6 +779,77 @@ agents its module also loads on the dev server when a command asks about a story
|
|
|
775
779
|
touches the page inside `mount`. A name that no control has (`control: "selct"`) is read as a custom control, and says so
|
|
776
780
|
beside the field.
|
|
777
781
|
|
|
782
|
+
### Controls by type, and from a library's preset
|
|
783
|
+
|
|
784
|
+
A control can take a prop in every story without any story naming it: by the prop's type, its name, or both. A library
|
|
785
|
+
registers its controls in its preset, so every catalogue that lists the preset gets them, with no `argTypes` to write:
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
// the library's preset (its `./forgebook` export)
|
|
789
|
+
export const acmePreset = (): ForgebookPreset =>
|
|
790
|
+
definePreset({
|
|
791
|
+
name: "acme-ui",
|
|
792
|
+
argControls: {
|
|
793
|
+
icon: {
|
|
794
|
+
module: "@acme/ui/forgebook/icon-picker", // mount and schema, drawn in the panel
|
|
795
|
+
frame: "@acme/ui/forgebook/icon-value", // toStoryValue, run in the story's frame
|
|
796
|
+
matches: { typeName: "IconComponent" }
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
strings: { "argControls.icon.none": "No icon" }
|
|
800
|
+
});
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
// @acme/ui/forgebook/icon-value
|
|
805
|
+
import type { ArgControlFrameModule } from "forgebook/arg-control";
|
|
806
|
+
import { ICONS } from "../src/icons";
|
|
807
|
+
|
|
808
|
+
export const toStoryValue: ArgControlFrameModule["toStoryValue"] = (name) =>
|
|
809
|
+
typeof name === "string" && Object.hasOwn(ICONS, name) ? ICONS[name] : undefined;
|
|
810
|
+
|
|
811
|
+
// optional: the story's own icon named back, so the picker shows what the story draws
|
|
812
|
+
export const toPanelValue: ArgControlFrameModule["toPanelValue"] = (icon) =>
|
|
813
|
+
Object.entries(ICONS).find(([, each]) => each === icon)?.[0];
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
- **What matches.** `matches` is one `{ typeName, propName }` or a list of them; each field given must hold. `typeName`
|
|
817
|
+
is the type as the props are read: as the source writes it (`IconComponent`), or any alias it is declared through
|
|
818
|
+
(`type ChipIcon = IconComponent`), an import's new name, with `undefined` and `null` left out, so `icon?: IconComponent`
|
|
819
|
+
and `icon?: IconComponent | undefined` match; a union matches only when every member goes by the name. `forgebook story
|
|
820
|
+
--json` writes the names a prop's type goes by as `typeNames` when they are more than its `type`. An event never
|
|
821
|
+
matches.
|
|
822
|
+
- **Which control wins.** A control a story's `argTypes` name for the prop wins over any match; a `description` or a
|
|
823
|
+
`label` alone keeps the matched control. Among controls that match, one that names the prop wins over one that names
|
|
824
|
+
only its type, then the catalogue's own before its presets', each in the order written. Only props the component has
|
|
825
|
+
get a control: no story gains a prop.
|
|
826
|
+
- **The value.** The panel, a link and an agent keep the control's JSON value (`"IconSearch"`), checked against its
|
|
827
|
+
`schema`. A `frame` module turns it into what the story receives, in the frame, with `toStoryValue(value, { name,
|
|
828
|
+
settings })`; a value it cannot turn, or a module that fails, leaves the story its own value and says why. Without a
|
|
829
|
+
`frame` module the story receives the JSON value. `toPanelValue`, optional, names a value the story gives itself.
|
|
830
|
+
- **Its code.** The Code tab and `forgebook story` write a value as the story receives it when the control's module
|
|
831
|
+
exports `toCode(value, argType)`, the expression the attribute holds: `toCode: (name) => name` writes
|
|
832
|
+
`icon={IconStar}`, not `icon="IconStar"`. Without it, or for a value it gives `undefined`, the JSON value is written;
|
|
833
|
+
the story file's own code for an arg stays as written until the panel sets it. It runs in the page and on the dev
|
|
834
|
+
server.
|
|
835
|
+
- **When it loads.** The module loads when the panel first shows a prop it takes, the frame module when a story's frame
|
|
836
|
+
is first given a value of it: neither on the catalogue's first load. A frame reads its story's component docs, to
|
|
837
|
+
know which props a control takes, only once a module may have work: the panel gives a value, the story's `argTypes`
|
|
838
|
+
name a control with a frame module, or the story gives itself a value plain data cannot show (an icon to name back).
|
|
839
|
+
A story of plain values that nobody edits loads neither.
|
|
840
|
+
- **Texts.** A control receives `strings` (always given; optional in the type, so its own tests need not build it): the
|
|
841
|
+
catalogue's `strings` whose key starts with `argControls.<name>.`, by the rest of the key. A preset words its control
|
|
842
|
+
there, and the catalogue's own `strings` replace a preset's.
|
|
843
|
+
- **The catalogue decides.** Its registration of a name replaces a preset's, and `argControls: { icon: false }` removes
|
|
844
|
+
it (a name no preset registers is an error, so a typo is not silent). A preset names its modules by absolute path or a
|
|
845
|
+
package's specifier, as its preview; no module is an address (`https:`, `data:`). A preset cannot take every prop of a
|
|
846
|
+
keyword type (`{ typeName: "string" }`) without a `propName`.
|
|
847
|
+
|
|
848
|
+
**A preset's control runs in the catalogue's page, with a plugin's trust**, as the catalogue's own do. A preset is
|
|
849
|
+
already code the catalogue runs on Node (its configuration calls it, and the dev server loads a control's module for
|
|
850
|
+
its `schema`); what changes is that its control also runs in the page. Its frame module runs in the stories' frames, as
|
|
851
|
+
its preview does.
|
|
852
|
+
|
|
778
853
|
## Docs pages
|
|
779
854
|
|
|
780
855
|
Guides, foundations and recipes are Markdown files with a little front matter, not MDX: a page is read at build time, and
|
|
@@ -1629,6 +1704,11 @@ its `frame-src`, in the preview's `frameDomains`).
|
|
|
1629
1704
|
- **`server.fs.deny` wins over your globs.** A file it refuses stays out of the catalogue, its search and "Used in", with
|
|
1630
1705
|
a warning. A file your globs name outside `server.fs.allow` is listed and served, as Vite serves any file a module of
|
|
1631
1706
|
the project imports.
|
|
1707
|
+
- **A missing file is a 404, never your app's page.** The catalogue's two pages are its own, so the dev server has no
|
|
1708
|
+
single-page fallback: a story's image under a path nothing serves answers 404, and the checks list it among their
|
|
1709
|
+
warnings by its path, without failing.
|
|
1710
|
+
Your app's `index.html` is never served, so its entry and every package it reaches stay unloaded. A Vite configuration
|
|
1711
|
+
of your own that runs the plugin and sets `appType` keeps it.
|
|
1632
1712
|
|
|
1633
1713
|
A command, an agent or `curl` sends no browser headers: `forgebook mcp`, `forgebook stories` and the agent API
|
|
1634
1714
|
(`/__forgebook/api/`) work as usual, and anyone who can reach the port can use them. With `--host` that means your whole
|
|
@@ -127,6 +127,11 @@ export const checkText = (report, style) => (report.a11y === "off" ? "Accessibil
|
|
|
127
127
|
result.view || "default",
|
|
128
128
|
`${problem.kind}: ${problem.message}`
|
|
129
129
|
])),
|
|
130
|
+
...(result.warnings ?? []).map((warning) => [
|
|
131
|
+
result.storyId,
|
|
132
|
+
result.view || "default",
|
|
133
|
+
`warning: ${warning.message}`
|
|
134
|
+
]),
|
|
130
135
|
...judgementLines(result)
|
|
131
136
|
]), { columns: [{ header: "Story" }, { header: "View", tone: "muted" }, { header: "Result" }] }, uncut(style));
|
|
132
137
|
/**
|
|
@@ -332,6 +337,7 @@ export const verifyText = (document, style) => {
|
|
|
332
337
|
const outdated = (document.outdated ?? []).map((each) => `${each.file} out of date: written for props ${each.written}, now ${each.current}. Fix: ${each.fix}`);
|
|
333
338
|
const notes = document.code.notes.map((each) => `${each.file}${each.line === undefined ? "" : `:${each.line}`} note: ${each.message}`);
|
|
334
339
|
const advice = (document.code.advice ?? []).map((each) => `${each.file}:${each.line} ${each.message}`);
|
|
340
|
+
const warnings = document.warnings.map((each) => `${each.path} warning: ${each.message}, in ${each.stories.length === 1 ? "1 story" : `${each.stories.length} stories`}`);
|
|
335
341
|
const results = [
|
|
336
342
|
...document.failures,
|
|
337
343
|
...document.notVerified.map((each) => ({ ...each, problems: [] }))
|
|
@@ -344,6 +350,7 @@ export const verifyText = (document, style) => {
|
|
|
344
350
|
...outdated,
|
|
345
351
|
...notes,
|
|
346
352
|
...advice,
|
|
353
|
+
...warnings,
|
|
347
354
|
...stories,
|
|
348
355
|
document.summary
|
|
349
356
|
].join("\n");
|
|
@@ -46,6 +46,11 @@ not report `partly-verified` as verified: say what `skipped` says. A file in `ui
|
|
|
46
46
|
`verify` again. `coverage: "through"` means it ran in another component's stories. A UI file is a component or a style
|
|
47
47
|
sheet by its extension, a story file, or a script that defines or draws a component; anything else does not fail.
|
|
48
48
|
|
|
49
|
+
A static file a story asks for that is not there (an image, a font, a style sheet answering 404) is a warning, not a
|
|
50
|
+
failure: `verify` lists it once in `warnings` with its `path` and the `stories` that asked for it, and says it in
|
|
51
|
+
`summary`; `check` gives it in each result's `warnings`. Name it when you report, and add the file to the public folder
|
|
52
|
+
if the change should have one. A module or a script that answers 404, and any error on the page, still fail.
|
|
53
|
+
|
|
49
54
|
A story file `forgebook new` wrote carries `// forgebook-props: <signature>`, the props it was written for. When the change
|
|
50
55
|
reaches it and its component's props no longer give that signature, `verify` lists it in `outdated` and fails: its args
|
|
51
56
|
may pass a prop that is gone or miss one now required. Check them against `story <id>`'s props, then set the line to
|
|
@@ -27,8 +27,13 @@ The framework's own guide (`forgebook guide <framework>`) has what differs in yo
|
|
|
27
27
|
value over the story's own; an empty text is `""`; a `Date` is `{ "forgebookDate": "<ISO>" }`. Give a person the `address` link to look at them: `forgebook shot` draws
|
|
28
28
|
a frame alone, with the story's own values.
|
|
29
29
|
- A custom control (`control: "custom"`, `custom` its name) takes a value that fits `schema`, in `controls`; set it as any
|
|
30
|
-
other. A new one is a module exporting `mount(el, { value, onChange, argType })`, returning an unmount, and
|
|
31
|
-
registered in `argControls` of `forgebook.config
|
|
30
|
+
other. A new one is a module exporting `mount(el, { value, onChange, argType, strings })`, returning an unmount, and
|
|
31
|
+
`schema`, registered in `argControls` of `forgebook.config` or of a library's preset; keep what touches the page inside
|
|
32
|
+
`mount`, so the commands can read it. Registered as `{ module, frame, matches: { typeName: "IconComponent" } }`, it takes
|
|
33
|
+
every prop of that type (or `propName`) in stories whose `argTypes` name no control for it, with no story code: a prop's
|
|
34
|
+
`typeNames` say the aliases its type goes through. The value stays JSON (an icon's name); the `frame` module's
|
|
35
|
+
`toStoryValue(value, argType)` turns it into what the story receives; the module's optional `toCode(value, argType)`
|
|
36
|
+
gives the expression the story's code writes (`icon={IconStar}`).
|
|
32
37
|
- A prop's TSDoc can ask for design tokens (`@token color`, a token's type or a part of its name), a story's arg with
|
|
33
38
|
`argTypes: { x: { control: { type: "token", of: "space", format: "var(--{name})" } } }`.
|
|
34
39
|
- Story ids are Storybook's (`inputs-readmore--open-by-default`, prefixed by the source's id); the commands also take
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
19
|
+
import { rm } from "node:fs/promises";
|
|
19
20
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
20
21
|
import { findConfigFile } from "../../src/vite/configFile.js";
|
|
21
22
|
import { gitOutput, gitOutputIn } from "../../src/vite/gitChange.js";
|
|
@@ -74,6 +75,13 @@ const twinModules = (from, to, root, folder) => {
|
|
|
74
75
|
symlinkSync(linkTarget(entry, root, folder), join(to, name), "junction");
|
|
75
76
|
}
|
|
76
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* How the base's folder is removed: tried again for a moment while it is still being written in. A server Vite closes
|
|
80
|
+
* while its first pre-bundle runs (a base with none of the stories asked for closes at once) cancels the run, but the
|
|
81
|
+
* bundler still writes its files into the cache folder inside, and the removal would fail on a folder not empty. The
|
|
82
|
+
* removal waits between tries without holding the process, so what is still writing can finish.
|
|
83
|
+
*/
|
|
84
|
+
const BASE_REMOVAL = { recursive: true, force: true, maxRetries: 10, retryDelay: 50 };
|
|
77
85
|
/** The prefix of the folders a base runs in, beside the repository. */
|
|
78
86
|
const folderPrefix = (root) => `.${basename(root)}-forgebook-base-`;
|
|
79
87
|
/**
|
|
@@ -189,8 +197,8 @@ export const runOnBase = async (config, base, ids, isAllVariants, runEnv) => {
|
|
|
189
197
|
finally {
|
|
190
198
|
// the links go first, so nothing that removes the worktree can follow one into the working tree
|
|
191
199
|
for (const twin of twins)
|
|
192
|
-
|
|
200
|
+
await rm(twin, BASE_REMOVAL);
|
|
193
201
|
await gitOutputIn(root, env, "worktree", "remove", "--force", folder).catch(() => undefined);
|
|
194
|
-
|
|
202
|
+
await rm(folder, BASE_REMOVAL);
|
|
195
203
|
}
|
|
196
204
|
};
|
|
@@ -198,6 +198,30 @@ const failuresOf = (results, links) => results
|
|
|
198
198
|
const storyLinks = links.get(storyId);
|
|
199
199
|
return { storyId, view, problems: found, ...(storyLinks === undefined ? {} : { links: storyLinks }) };
|
|
200
200
|
});
|
|
201
|
+
/**
|
|
202
|
+
* The warnings of the pairs that ran, one per file, with the stories that asked for it.
|
|
203
|
+
*
|
|
204
|
+
* @param results - the pairs that ran.
|
|
205
|
+
*/
|
|
206
|
+
const warningsOf = (results) => {
|
|
207
|
+
const byPath = new Map();
|
|
208
|
+
for (const result of results)
|
|
209
|
+
for (const warning of result.warnings ?? []) {
|
|
210
|
+
const found = byPath.get(warning.path) ?? { warning, stories: new Set() };
|
|
211
|
+
found.stories.add(result.storyId);
|
|
212
|
+
byPath.set(warning.path, found);
|
|
213
|
+
}
|
|
214
|
+
return [...byPath.values()].map(({ warning, stories }) => ({ ...warning, stories: [...stories] }));
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* The end of the verdict's line for the warnings: a missing file does not change the verdict, so the line names it, so
|
|
218
|
+
* it is not missed; nothing without any.
|
|
219
|
+
*
|
|
220
|
+
* @param warnings - the warnings.
|
|
221
|
+
*/
|
|
222
|
+
const warningWords = (warnings) => warnings.length === 0
|
|
223
|
+
? ""
|
|
224
|
+
: `; ${counted(warnings.length, "warning")}: ${warnings.map((each) => `${each.path} is missing`).join(", ")}`;
|
|
201
225
|
/** What the comparison with the base leaves unchecked: a base that could not run, stories it never saw. */
|
|
202
226
|
const baseSkips = (compared) => [
|
|
203
227
|
...(compared.notCompared === undefined
|
|
@@ -226,6 +250,7 @@ export const verifyDocument = (input) => {
|
|
|
226
250
|
const uiFiles = changed.uiFiles.map((file) => verifiedFile(file, ran, notRun));
|
|
227
251
|
const links = new Map(changed.stories.map((story) => [story.id, story.links]));
|
|
228
252
|
const failures = failuresOf(results, links);
|
|
253
|
+
const warnings = warningsOf(results);
|
|
229
254
|
const notVerified = results.flatMap(({ storyId, view, notVerified: reason }) => {
|
|
230
255
|
if (reason === undefined)
|
|
231
256
|
return [];
|
|
@@ -272,6 +297,7 @@ export const verifyDocument = (input) => {
|
|
|
272
297
|
ran: seenResults.length,
|
|
273
298
|
failures,
|
|
274
299
|
notVerified,
|
|
300
|
+
warnings,
|
|
275
301
|
code: {
|
|
276
302
|
checked: code.checked,
|
|
277
303
|
problems: codeProblems,
|
|
@@ -292,6 +318,6 @@ export const verifyDocument = (input) => {
|
|
|
292
318
|
const old = existingProblems === 0
|
|
293
319
|
? ""
|
|
294
320
|
: `; ${counted(existingProblems, "problem")} the stories had before the change not counted (existing; --all-problems counts them)`;
|
|
295
|
-
const summary = `${summaryOf({ ...body, verdict, skipped: allSkipped }, catalogueProblems)}${old}`;
|
|
321
|
+
const summary = `${summaryOf({ ...body, verdict, skipped: allSkipped }, catalogueProblems)}${old}${warningWords(warnings)}`;
|
|
296
322
|
return { schema: 2, verdict, summary, skipped: allSkipped, ...body };
|
|
297
323
|
};
|
package/dist/src/argControl.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A custom control's contract: a module a catalogue registers by name (`argControls`
|
|
3
|
-
* uses as `argTypes: { x: { control: "name" } }
|
|
2
|
+
* A custom control's contract: a module a catalogue or a library's preset registers by name (`argControls`) and a story
|
|
3
|
+
* uses as `argTypes: { x: { control: "name" } }`, or that takes the props of a type in every story (`matches`); and its
|
|
4
|
+
* frame module, which turns the control's value into the story's.
|
|
4
5
|
*
|
|
5
6
|
* @remarks
|
|
6
7
|
* `mount` rather than a component, so a team writes one in whatever its stories are written in: the shell knows nothing of
|
|
7
8
|
* how it draws. The value is always JSON with a declared schema, so a link keeps working and an agent can set it with no
|
|
8
|
-
* UI. The module runs in the shell's page, with a plugin's trust.
|
|
9
|
+
* UI. The module runs in the shell's page, with a plugin's trust; the frame module in the stories' frames, as a preview.
|
|
9
10
|
*
|
|
10
11
|
* @module
|
|
11
12
|
*/
|
|
@@ -84,6 +85,13 @@ export type ArgControlProps = {
|
|
|
84
85
|
readonly onChange: (value: JsonValue | undefined) => void;
|
|
85
86
|
/** What the story says of the arg. */
|
|
86
87
|
readonly argType: ControlArgType;
|
|
88
|
+
/**
|
|
89
|
+
* The control's texts as the catalogue words them: its `strings` (a preset's, then the catalogue's own over them) whose
|
|
90
|
+
* key starts with `argControls.<name>.`, by the rest of the key (`none` for `argControls.icon.none`). A control falls
|
|
91
|
+
* back to its own words for a key it does not find. Always given by the panel; optional so a control's own tests need
|
|
92
|
+
* not build it.
|
|
93
|
+
*/
|
|
94
|
+
readonly strings?: Readonly<Record<string, string>>;
|
|
87
95
|
};
|
|
88
96
|
/** A custom control's module. */
|
|
89
97
|
export type ArgControlModule = {
|
|
@@ -97,4 +105,44 @@ export type ArgControlModule = {
|
|
|
97
105
|
readonly mount: (element: HTMLElement, props: ArgControlProps) => () => void;
|
|
98
106
|
/** What its value looks like: a link's value and each `onChange` are checked against it. */
|
|
99
107
|
readonly schema: ValueSchema;
|
|
108
|
+
/**
|
|
109
|
+
* The code a value is written as in the story's code (the Code tab, `forgebook story`), when it is not the value as
|
|
110
|
+
* JSON: the expression the attribute holds, `IconStar` for the icon the name `"IconStar"` gives the story (written
|
|
111
|
+
* `icon={IconStar}`). None, or `undefined` for a value, writes the JSON value. Runs in the page and on the dev server,
|
|
112
|
+
* so it reads nothing of either.
|
|
113
|
+
*
|
|
114
|
+
* @param value - the panel's value.
|
|
115
|
+
* @param argType - what the story says of the arg.
|
|
116
|
+
* @returns the expression, as code.
|
|
117
|
+
*/
|
|
118
|
+
readonly toCode?: (value: JsonValue, argType: ControlArgType) => string | undefined;
|
|
119
|
+
};
|
|
120
|
+
/** What a frame module is told of the arg whose value it turns into the story's. */
|
|
121
|
+
export type FrameArgType = {
|
|
122
|
+
/** The arg's name. */
|
|
123
|
+
readonly name: string;
|
|
124
|
+
/** The control's `settings` the story's `argTypes` write, as JSON; none when they write none. */
|
|
125
|
+
readonly settings?: JsonValue;
|
|
126
|
+
};
|
|
127
|
+
/** A custom control's frame module: loaded in a story's frame when the panel first gives it a value of that control. */
|
|
128
|
+
export type ArgControlFrameModule = {
|
|
129
|
+
/**
|
|
130
|
+
* The value the story receives for the panel's: a name turned into the component it names, say. Links and agents keep
|
|
131
|
+
* the JSON value; this runs only in the frame, after the shell checked the value against the control's schema. A link
|
|
132
|
+
* chose it, so read it as any JSON: look a name up among your own entries (`Object.hasOwn`), never through a prototype.
|
|
133
|
+
*
|
|
134
|
+
* @param value - the panel's value, as JSON.
|
|
135
|
+
* @param argType - the arg.
|
|
136
|
+
* @returns what the story receives; `undefined` gives the story no value.
|
|
137
|
+
*/
|
|
138
|
+
readonly toStoryValue: (value: JsonValue, argType: FrameArgType) => unknown;
|
|
139
|
+
/**
|
|
140
|
+
* The panel's value for a value the story gives itself (the icon its `args` name back into its name), so the control
|
|
141
|
+
* shows what the story draws; none leaves the control without one, as for any value JSON cannot hold.
|
|
142
|
+
*
|
|
143
|
+
* @param value - the story's own value.
|
|
144
|
+
* @param argType - the arg.
|
|
145
|
+
* @returns the panel's value, which the control's schema must take; `undefined` when it has none.
|
|
146
|
+
*/
|
|
147
|
+
readonly toPanelValue?: (value: unknown, argType: FrameArgType) => JsonValue | undefined;
|
|
100
148
|
};
|
package/dist/src/argTypes.js
CHANGED
|
@@ -97,9 +97,11 @@ const withArgType = (prop, argType, own) => {
|
|
|
97
97
|
const { control } = argType;
|
|
98
98
|
// a control the panel cannot draw makes the prop one it lists for reading, never one that takes a text
|
|
99
99
|
const isReadOnly = control === "none";
|
|
100
|
-
|
|
100
|
+
// a control the story names wins over the one the catalogue gives the prop's type
|
|
101
|
+
const { customControl: _typed, ...untyped } = prop;
|
|
102
|
+
const { canTakeText: _canTakeText, ...rest } = untyped;
|
|
101
103
|
// a control the story names decides what its value is: the type's shape no longer draws nor checks it
|
|
102
|
-
const { shape: _shape, ...shapeless } = isReadOnly || argType.customControl !== undefined ? rest :
|
|
104
|
+
const { shape: _shape, ...shapeless } = isReadOnly || argType.customControl !== undefined ? rest : untyped;
|
|
103
105
|
return {
|
|
104
106
|
...(control === undefined && argType.customControl === undefined ? prop : shapeless),
|
|
105
107
|
...(control === undefined ? {} : { kind: KIND_OF_CONTROL[control], control }),
|
|
@@ -129,12 +129,24 @@ export type PropDocs = {
|
|
|
129
129
|
* union by the types it happened to meet first, so it changed between reads of a program kept in memory.
|
|
130
130
|
*/
|
|
131
131
|
readonly type: string;
|
|
132
|
+
/**
|
|
133
|
+
* The names its type goes by, when they are more than `type`: the name written, the name it was declared with when an
|
|
134
|
+
* import renamed it, and each alias it is declared through (`ButtonIcon`, `IconComponent` for
|
|
135
|
+
* `type ButtonIcon = IconComponent`), without `undefined` and `null`; for a union, the names every member goes by. Left
|
|
136
|
+
* out when `type` says them all.
|
|
137
|
+
*/
|
|
138
|
+
readonly typeNames?: readonly string[];
|
|
132
139
|
/** The first paragraph of its TSDoc; empty when it has none. */
|
|
133
140
|
readonly description: string;
|
|
134
141
|
/** Its `@defaultValue`, when the TSDoc gives one. */
|
|
135
142
|
readonly defaultValue?: string;
|
|
136
143
|
/** Whether the prop must be passed. */
|
|
137
144
|
readonly isRequired: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* The custom control the catalogue gives it by its type or its name (a control's `matches`), when one does: the
|
|
147
|
+
* plugin adds it as it serves the docs, so the panel draws it in every story that names no control for the prop.
|
|
148
|
+
*/
|
|
149
|
+
readonly customControl?: string;
|
|
138
150
|
/** For a choice, its values in the order of the type. */
|
|
139
151
|
readonly choices?: readonly (string | number)[];
|
|
140
152
|
/** For an event, its signature. */
|
|
@@ -32,7 +32,11 @@ const isMemberDocs = (value) => isRecord(value) &&
|
|
|
32
32
|
(value["defaultValue"] === undefined || typeof value["defaultValue"] === "string");
|
|
33
33
|
/** Whether a value is a note with a reason: what the reference reads first. */
|
|
34
34
|
const isNote = (value) => isRecord(value) && typeof value["reason"] === "string";
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Whether a value is one prop's docs. Some fields are left to what reads them, as a choice's values and a signature are:
|
|
37
|
+
* a custom control's name is only looked up among the catalogue's own controls, so a wrong one finds none and says so
|
|
38
|
+
* beside the field; the shell reads no type's names.
|
|
39
|
+
*/
|
|
36
40
|
const isPropDocs = (value) => isRecord(value) &&
|
|
37
41
|
typeof value["name"] === "string" &&
|
|
38
42
|
PROP_KINDS.some((kind) => kind === value["kind"]) &&
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a custom control is told of its arg, and the code its values are written as in a story's code: the expression a
|
|
3
|
+
* control's `toCode` gives (`IconStar` for the name `"IconStar"`), for the Code tab and `forgebook story`. Pure.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { ArgControlModule, ControlArgType } from "./argControl.ts";
|
|
8
|
+
import type { PanelProp } from "./argTypes.ts";
|
|
9
|
+
import { type PanelArgs } from "./messages.ts";
|
|
10
|
+
/**
|
|
11
|
+
* What a control is told of its arg.
|
|
12
|
+
*
|
|
13
|
+
* @param prop - the prop, with the story's `argTypes` laid over it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const controlArgTypeOf: (prop: PanelProp) => ControlArgType;
|
|
16
|
+
/** A control's `toCode`, by control: what writes its values as code. */
|
|
17
|
+
export type ControlCoders = Readonly<Record<string, ArgControlModule["toCode"]>>;
|
|
18
|
+
/**
|
|
19
|
+
* The code each arg of a custom control is written as, by arg: the expression its control's `toCode` gives for its
|
|
20
|
+
* value. An arg without a value, with the panel's "no value", whose control writes no code, or whose `toCode` throws
|
|
21
|
+
* or gives nothing, is written from its value as before.
|
|
22
|
+
*
|
|
23
|
+
* @param props - the props the panel sets, with the story's `argTypes` laid over them.
|
|
24
|
+
* @param args - the values the code is written from.
|
|
25
|
+
* @param coders - each control's `toCode`, by control.
|
|
26
|
+
*/
|
|
27
|
+
export declare const controlCodeOf: (props: readonly PanelProp[], args: PanelArgs, coders: ControlCoders) => Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isArgValue, isUnsetArg } from "./messages.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a control is told of its arg.
|
|
4
|
+
*
|
|
5
|
+
* @param prop - the prop, with the story's `argTypes` laid over it.
|
|
6
|
+
*/
|
|
7
|
+
export const controlArgTypeOf = (prop) => ({
|
|
8
|
+
name: prop.name,
|
|
9
|
+
label: prop.label ?? prop.name,
|
|
10
|
+
description: prop.description,
|
|
11
|
+
...(prop.choices === undefined ? {} : { options: prop.choices }),
|
|
12
|
+
...(prop.min === undefined ? {} : { min: prop.min }),
|
|
13
|
+
...(prop.max === undefined ? {} : { max: prop.max }),
|
|
14
|
+
...(prop.step === undefined ? {} : { step: prop.step }),
|
|
15
|
+
// the frame's word, read here since only a control needs it: what is no plain data never reaches a control
|
|
16
|
+
...(prop.settings === undefined || !isArgValue(prop.settings) ? {} : { settings: prop.settings })
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* The code each arg of a custom control is written as, by arg: the expression its control's `toCode` gives for its
|
|
20
|
+
* value. An arg without a value, with the panel's "no value", whose control writes no code, or whose `toCode` throws
|
|
21
|
+
* or gives nothing, is written from its value as before.
|
|
22
|
+
*
|
|
23
|
+
* @param props - the props the panel sets, with the story's `argTypes` laid over them.
|
|
24
|
+
* @param args - the values the code is written from.
|
|
25
|
+
* @param coders - each control's `toCode`, by control.
|
|
26
|
+
*/
|
|
27
|
+
export const controlCodeOf = (props, args, coders) => Object.fromEntries(props.flatMap((prop) => {
|
|
28
|
+
const control = prop.customControl;
|
|
29
|
+
const value = Object.hasOwn(args, prop.name) ? args[prop.name] : undefined;
|
|
30
|
+
const toCode = control !== undefined && Object.hasOwn(coders, control) ? coders[control] : undefined;
|
|
31
|
+
if (toCode === undefined || value === undefined || isUnsetArg(value))
|
|
32
|
+
return [];
|
|
33
|
+
try {
|
|
34
|
+
const code = toCode(value, controlArgTypeOf(prop));
|
|
35
|
+
return typeof code === "string" && code !== "" ? [[prop.name, code]] : [];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// the control's code is its own: a failure writes the value as JSON, as a control without `toCode`
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type PanelArgs } from "../messages.ts";
|
|
2
|
+
import { panelArgTypesOf } from "../storyArgTypes.ts";
|
|
3
|
+
import type { ControlFrameLoaders } from "./panelControls.ts";
|
|
4
|
+
/** What the frame modules made of the values. */
|
|
5
|
+
export type ControlValues = {
|
|
6
|
+
/** What the story receives, by arg, over what the panel's JSON gives it. */
|
|
7
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
8
|
+
/** The args whose value could not be made: the story keeps its own. */
|
|
9
|
+
readonly dropped: readonly string[];
|
|
10
|
+
/** The story's own values as the panel shows them, by arg, for those a frame module names. */
|
|
11
|
+
readonly own: PanelArgs;
|
|
12
|
+
/** What went wrong, one sentence per failure. */
|
|
13
|
+
readonly problems: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
/** The story, as the frame modules read it. */
|
|
16
|
+
export type ControlStory = {
|
|
17
|
+
/** Its own args. */
|
|
18
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
19
|
+
/** Its `argTypes`: the controls they name, and a control's `settings`. */
|
|
20
|
+
readonly argTypes: Parameters<typeof panelArgTypesOf>[0];
|
|
21
|
+
/** Loads its component's docs, which name the controls the catalogue gives props by type; none without docs. */
|
|
22
|
+
readonly loadDocs?: (() => Promise<unknown>) | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The custom control of each arg, as the panel draws it: a control the story's `argTypes` name, else the one the
|
|
26
|
+
* catalogue gives the prop by type, unless the `argTypes` name another control for it.
|
|
27
|
+
*
|
|
28
|
+
* @param argTypes - the story's `argTypes`, as the panel reads them.
|
|
29
|
+
* @param docs - its component's docs, as the plugin served them.
|
|
30
|
+
*/
|
|
31
|
+
export declare const argControlsOf: (argTypes: ReturnType<typeof panelArgTypesOf>, docs: unknown) => Readonly<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* The values of the controls with a frame module, as the story and the panel take them.
|
|
34
|
+
*
|
|
35
|
+
* @param args - the panel's values, checked by the shell.
|
|
36
|
+
* @param loaders - the frame modules, by control.
|
|
37
|
+
* @param story - the story's own args, its `argTypes` and its docs.
|
|
38
|
+
*/
|
|
39
|
+
export declare const controlValuesOf: (args: PanelArgs, loaders: ControlFrameLoaders, story: ControlStory) => Promise<ControlValues>;
|