forgebook 0.2.0 → 0.3.0

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 (94) hide show
  1. package/README.md +112 -5
  2. package/dist/bin/forgebook/agentLoopCommands.mjs +3 -0
  3. package/dist/bin/forgebook/agentText.d.ts +2 -2
  4. package/dist/bin/forgebook/agentText.js +15 -13
  5. package/dist/bin/forgebook/agents.md +21 -6
  6. package/dist/bin/forgebook/mcpTools.js +1 -1
  7. package/dist/bin/forgebook/newStoriesFile.js +7 -2
  8. package/dist/bin/forgebook-docs/collectDocs.js +1 -1
  9. package/dist/src/argsProps.d.ts +32 -0
  10. package/dist/src/argsProps.js +69 -0
  11. package/dist/src/catalogue.d.ts +48 -5
  12. package/dist/src/catalogue.js +47 -8
  13. package/dist/src/frame/mountFrame.js +26 -7
  14. package/dist/src/frame/touchCaret.d.ts +43 -0
  15. package/dist/src/frame/touchCaret.js +195 -0
  16. package/dist/src/frame/touchEvents.d.ts +3 -2
  17. package/dist/src/frame/touchEvents.js +17 -18
  18. package/dist/src/frame/touchLayer.js +48 -7
  19. package/dist/src/propGroups.d.ts +66 -3
  20. package/dist/src/propGroups.js +67 -13
  21. package/dist/src/shell/ApiReference.d.ts +9 -4
  22. package/dist/src/shell/ApiReference.js +47 -39
  23. package/dist/src/shell/AudienceSections.js +1 -1
  24. package/dist/src/shell/ComponentPage.d.ts +5 -2
  25. package/dist/src/shell/ComponentPage.js +4 -18
  26. package/dist/src/shell/DocBlocks.js +1 -1
  27. package/dist/src/shell/PropRows.d.ts +42 -0
  28. package/dist/src/shell/PropRows.js +60 -0
  29. package/dist/src/shell/PropsTab.d.ts +14 -10
  30. package/dist/src/shell/PropsTab.js +18 -58
  31. package/dist/src/shell/RequiredFirstSwitch.d.ts +16 -0
  32. package/dist/src/shell/RequiredFirstSwitch.js +8 -0
  33. package/dist/src/shell/Stage.js +11 -23
  34. package/dist/src/shell/StoryCoverage.js +4 -1
  35. package/dist/src/shell/docPage.css +6 -0
  36. package/dist/src/shell/dockTabs.d.ts +7 -5
  37. package/dist/src/shell/dockTabs.js +4 -4
  38. package/dist/src/shell/reportedPanel.d.ts +12 -0
  39. package/dist/src/shell/reportedPanel.js +19 -0
  40. package/dist/src/shell/shell.css +56 -1
  41. package/dist/src/shell/strings.d.ts +2 -0
  42. package/dist/src/shell/strings.js +2 -0
  43. package/dist/src/shell/usePanelDocs.d.ts +11 -0
  44. package/dist/src/shell/usePanelDocs.js +29 -0
  45. package/dist/src/shell/useRequiredFirst.d.ts +27 -0
  46. package/dist/src/shell/useRequiredFirst.js +67 -0
  47. package/dist/src/storyCheck.d.ts +2 -0
  48. package/dist/src/storyCheck.js +2 -1
  49. package/dist/src/storyGlobals.js +2 -1
  50. package/dist/src/storyTree.d.ts +11 -1
  51. package/dist/src/storyTree.js +3 -1
  52. package/dist/src/viewState.d.ts +7 -0
  53. package/dist/src/viewState.js +13 -0
  54. package/dist/src/vite/agent.types.d.ts +12 -0
  55. package/dist/src/vite/agentApi.js +6 -1
  56. package/dist/src/vite/agentRoutes.js +18 -4
  57. package/dist/src/vite/builtTexts.d.ts +2 -2
  58. package/dist/src/vite/builtTexts.js +7 -6
  59. package/dist/src/vite/configFile.d.ts +3 -2
  60. package/dist/src/vite/configFile.js +63 -14
  61. package/dist/src/vite/docPagesModule.d.ts +10 -7
  62. package/dist/src/vite/docPagesModule.js +14 -11
  63. package/dist/src/vite/docsExtractor.d.ts +6 -0
  64. package/dist/src/vite/docsExtractor.js +13 -0
  65. package/dist/src/vite/fileAccess.d.ts +51 -0
  66. package/dist/src/vite/fileAccess.js +109 -0
  67. package/dist/src/vite/forgebookPlugin.d.ts +1 -1
  68. package/dist/src/vite/forgebookPlugin.js +61 -96
  69. package/dist/src/vite/framesServer.js +21 -5
  70. package/dist/src/vite/generatedModules.d.ts +23 -0
  71. package/dist/src/vite/generatedModules.js +58 -0
  72. package/dist/src/vite/hotSnippets.js +7 -3
  73. package/dist/src/vite/indexStoryFile.d.ts +2 -2
  74. package/dist/src/vite/indexStoryFile.js +2 -2
  75. package/dist/src/vite/indexedParameters.d.ts +1 -1
  76. package/dist/src/vite/indexedParameters.js +21 -1
  77. package/dist/src/vite/originGuard.d.ts +14 -4
  78. package/dist/src/vite/originGuard.js +15 -6
  79. package/dist/src/vite/pages.d.ts +3 -2
  80. package/dist/src/vite/pages.js +23 -16
  81. package/dist/src/vite/pluginIds.d.ts +9 -2
  82. package/dist/src/vite/pluginIds.js +21 -6
  83. package/dist/src/vite/pluginModules.d.ts +39 -10
  84. package/dist/src/vite/pluginModules.js +67 -22
  85. package/dist/src/vite/prebundled.js +2 -1
  86. package/dist/src/vite/searchModule.d.ts +6 -4
  87. package/dist/src/vite/searchModule.js +8 -7
  88. package/dist/src/vite/storyFilesModule.d.ts +59 -27
  89. package/dist/src/vite/storyFilesModule.js +88 -58
  90. package/dist/src/vite/storyPatterns.d.ts +39 -0
  91. package/dist/src/vite/storyPatterns.js +100 -0
  92. package/dist/src/vite/textQueries.d.ts +7 -1
  93. package/dist/src/vite/textQueries.js +7 -1
  94. package/package.json +2 -2
package/README.md CHANGED
@@ -107,6 +107,28 @@ npx forgebook build # static files in dist/, for any web server
107
107
  Badge's page shows its stories, each in its own frame, and an API table read from `BadgeProps`: `label` required,
108
108
  `tone` with its three values and its default, each with its TSDoc.
109
109
 
110
+ ### Which files
111
+
112
+ `stories` (and `docs`, for [docs pages](#docs-pages)) lists globs from the configuration's folder. A glob that starts
113
+ with `!` leaves out the files it matches from what the others list, wherever it stands in the list, as Vite's
114
+ `import.meta.glob` and fast-glob read it:
115
+
116
+ ```ts
117
+ stories: [
118
+ "src/**/*.stories.tsx",
119
+ "src/**/*-story.tsx",
120
+ // drafts and the components still being moved in: listed by neither glob above
121
+ "!src/**/drafts/**",
122
+ "!src/components/Legacy*/**"
123
+ ]
124
+ ```
125
+
126
+ A `!` glob is written from the same folder and with the same syntax as the others; one that names a folder
127
+ (`!src/legacy`) leaves out every file in it. A file two globs list is listed once, its title made from the first one
128
+ (when its meta writes none). A list of `!` globs alone lists nothing, and the catalogue refuses it. Adding a file a `!`
129
+ glob leaves out, while the catalogue runs, changes nothing on the page. A file Vite's `server.fs.deny` refuses stays out
130
+ too, with a warning.
131
+
110
132
  ## What a catalogue gives you
111
133
 
112
134
  - **One frame per story**, so a story's styles and globals never leak into another, and its address opens it alone.
@@ -139,6 +161,8 @@ Badge's page shows its stories, each in its own frame, and an API table read fro
139
161
  - **Props in groups**: a component with many props lists them under the groups their TSDoc names (`@group Paging`),
140
162
  and a prop typed with a documented object (`texts?: FinderTexts`) unfolds to list its members
141
163
  ([below](#props-in-groups)).
164
+ - **Required props first**: what every use must set comes first, in each group or in a section of its own, and a
165
+ reader turns the ordering off with a switch ([below](#required-props-first)).
142
166
  - **Shared values, checked**: the values set in the Props tab stay in the address (`args=`), so a link shares them.
143
167
  Since whoever writes a link chooses them, a story receives only the props it declares (its component's props, its
144
168
  `argTypes`, its own args) that the panel lets a person set, each with a value of that prop's type; anything else
@@ -214,7 +238,11 @@ The Touch switch (in View, `m` by default) lets a story be tried as on a phone,
214
238
  `navigator.maxTouchPoints` says 5;
215
239
  - the cursor is a round dot; Shift fixes a centre, and a press then puts a second finger mirrored around it, so a drag
216
240
  pinches and rotates;
217
- - a drag scrolls what it can, as the `touch-action` on the way allows, and flings on.
241
+ - a drag scrolls what it can, as the `touch-action` on the way allows, and flings on;
242
+ - a tap on a text field (an input, a textarea, a contenteditable) focuses it and puts the caret under the finger, a
243
+ drag or a long press there does not, and a handler that cancels the tap leaves it unfocused, as on a phone; the
244
+ keyboard then types and selects in it as usual (in a closed shadow root, or on a non-editable part of a
245
+ contenteditable, a press focuses at once and the caret is the browser's, the simulation not reaching them).
218
246
 
219
247
  A story reads the switch in its globals (`context.globals.touch`, `"mouse"` or `"touch"`) to draw only the version
220
248
  that fits the pointer, and follows the switch live; a story fixes it with `globals: { touch: "touch" }`.
@@ -225,9 +253,9 @@ its example's bar turns that off for it; on its own page, the Touch switch is th
225
253
  catalogue is open, and the address keeps the switch the other stories follow.
226
254
 
227
255
  **It is a simulation.** Events made by a page are not trusted, so the browser's own reactions do not follow them:
228
- its native scrolling (the drag's scrolling is an approximation, without the rubber band), focus and selection from a
229
- touch, and `touch-action` applied by the browser. In Safari and Firefox the stories get pointer events only (only
230
- Chromium lets a page build touch events). A virtual keyboard, the viewport shrinking under it, safe areas and real iOS
256
+ its native scrolling (the drag's scrolling is an approximation, without the rubber band), selecting a word or the page's
257
+ text with a finger, and `touch-action` applied by the browser. In Safari and Firefox the stories get pointer events only (only
258
+ Chromium lets a page build touch events), and Firefox does not place the caret inside an email or number field. A virtual keyboard, the viewport shrinking under it, safe areas and real iOS
231
259
  Safari need a device. For real touch events and native scrolling, `forgebook dev --touch` opens the catalogue in a
232
260
  Chromium window with Chromium's own touch emulation (it needs `@forgebook/testing` and Playwright, as `check` does).
233
261
  `controls: { touch: false }` takes the switch away.
@@ -409,6 +437,34 @@ export type FinderProps = {
409
437
  - With `eslint-plugin-tsdoc`, declare the tag in your `tsdoc.json`:
410
438
  `{ "tagName": "@group", "syntaxKind": "block", "allowMultiple": false }`.
411
439
 
440
+ ## Required props first
441
+
442
+ The API reference and the Props tab list a component's required props before the others, so a reader sees at once
443
+ what every use must set. Where they go is `requiredFirst`:
444
+
445
+ ```ts
446
+ // forgebook.config.ts: every component
447
+ export default defineConfig({ requiredFirst: "section" });
448
+
449
+ // a component's meta, for its page and its stories; or one story, for its own Props tab
450
+ export default { component: Finder, parameters: { requiredFirst: false } } satisfies Meta<typeof Finder>;
451
+ ```
452
+
453
+ - `"in-groups"`, the default: the required props first inside each group, and among the props that name no group and
454
+ the events; the others keep the order of the source. In the Props tab, first among each kind of control.
455
+ - `"section"`: one "Required" section before every group, all the required props in it, each naming its group (an
456
+ event says "Event"; the Props tab, which lists no events, leaves them to the Events tab); then the groups with the
457
+ others. A component whose props name no group has nothing to section, so it reads as
458
+ `"in-groups"`.
459
+ - `false`: the order of the source.
460
+ - The narrowest wins: a story's over its meta's, a meta's over the configuration's. Write it plainly (the index reads
461
+ it); anything else is reported where it is written.
462
+ - A reader's "Required first" switch, in the header of both tables, turns the ordering on or off for the whole
463
+ catalogue, kept in the browser: off, the order of the source; on, the developer's layout (`"in-groups"` where the
464
+ developer chose `false` everywhere). The switch never picks between the two layouts.
465
+ - Agents get the props in the order of the source and the names of the required ones at the top of
466
+ `forgebook story --json` and `get_story` (`requiredProps`): the setting shapes the pages only.
467
+
412
468
  ## Docs pages
413
469
 
414
470
  Guides, foundations and recipes are Markdown files with a little front matter, not MDX: a page is read at build time, and
@@ -711,7 +767,9 @@ its name.
711
767
  To try it on a Storybook project, run `npx forgebook init --from storybook` in the folder that holds `.storybook`. It
712
768
  writes `forgebook.config.ts`: the story globs of `main` (the MDX pages left out), the preview as Forgebook's preview,
713
769
  and a switch for each toolbar global of the preview, applied by code as its decorators expect. `main` is loaded, so a
714
- computed `stories` list is read too; the preview is read from its source, so only `globalTypes` written as plain values
770
+ computed `stories` list is read too, as the files it names (a list made with fast-glob and its `ignore`, for one,
771
+ arrives with the ignored files already out); to keep the exclusion as globs, write it as `!` globs
772
+ ([above](#which-files)) in place of the files. The preview is read from its source, so only `globalTypes` written as plain values
715
773
  become switches. It reads React, Vue, Svelte, Preact, Solid, Web Components and plain HTML on Vite
716
774
  (`@storybook/react-vite`, `@storybook/vue3-vite`, `@storybook/svelte-vite`, `@storybook/sveltekit` (whose
717
775
  `sveltekit_experimental` parameters `@forgebook/sveltekit` reads as they are),
@@ -767,6 +825,35 @@ A monorepo that points its packages at their sources with an export condition (`
767
825
  `forgebook` command: its dev server reads the stories and previews it answers tools with on Node under the same
768
826
  conditions, those of other runtimes (`browser`) aside, unless a plugin names the server's own.
769
827
 
828
+ ## A lighter catalogue
829
+
830
+ Two features cost the most and are on by default: the accessibility check, which ships axe (about 180 KB gzipped in a
831
+ build), and the props read from the types, whose TypeScript programs are most of what the dev server holds on a first
832
+ start. A catalogue that does not want them turns them off:
833
+
834
+ ```ts
835
+ export default defineConfig({
836
+ framework: react(),
837
+ title: "Acme",
838
+ stories: ["src/**/*.stories.tsx"],
839
+ a11y: false,
840
+ componentDocs: false
841
+ });
842
+ ```
843
+
844
+ - `a11y: false` checks nothing: no Accessibility tab and no accessibility column on a tester's page, axe neither written
845
+ in a build nor offered to the frames (whose code that loads it is fetched only on a check), and the generated tests,
846
+ `forgebook check`, `check-page` and their tools leave axe out (their reports say `"a11y": "off"`). `tabs: { a11y: false }`
847
+ only hides the tab and keeps the rest. A preset that turns the check off keeps it off, whatever rules the catalogue writes.
848
+ - `componentDocs: false` starts no TypeScript program: no API reference and no `props` fence on a page, no "Used in", and
849
+ the search finds names, stories and docs pages but no props. The Props tab still works: its controls come from the
850
+ story's args and `argTypes`, as Storybook's do without a docgen (a text, a number, a toggle from each arg's value, an
851
+ action as an event); a prop the story gives no arg is not listed. A shared link is then checked against those kinds:
852
+ where the story gives a text, a link may set any text, even for a prop whose type takes a few values only, so give
853
+ such a prop `argTypes` options (`{ control: "select", options: [...] }`) to keep a link to them. `forgebook story` answers `"componentDocs": "off"`
854
+ with no `docs`, and `forgebook changed` says it finds no story through the imports. `forgebook docs` and `forgebook new`
855
+ still read the types when run, since they are asked for that. `usedIn: true` with it is an error.
856
+
770
857
  ## Commands
771
858
 
772
859
  | Command | What it does |
@@ -931,6 +1018,26 @@ An embed (`?embed`) and an MCP client's inline preview are pages of the catalogu
931
1018
  frames' origin in turn, as the catalogue does: a page or a host that frames them names the catalogue's origin only (in
932
1019
  its `frame-src`, in the preview's `frameDomains`).
933
1020
 
1021
+ ### What the dev server gives a client
1022
+
1023
+ `forgebook dev` listens on localhost unless you pass `--host`, and on both of its ports it answers with these limits:
1024
+
1025
+ - **Another site's page gets nothing it can read.** A request a browser marks as another site's (`Sec-Fetch-Site`), or
1026
+ with a foreign `Origin`, is refused for every module, file and API. A link or a frame from elsewhere can open the
1027
+ catalogue's page, the frame page, an inline preview and a file of the public folder, whose content that site cannot
1028
+ read. Vite checks `Host`
1029
+ first (`server.allowedHosts`, plain HTTP only), which stops a site that reaches `127.0.0.1` through its own domain.
1030
+ - **Only the files your project names are listed or read.** A catalogue's `stories` and `docs`, and the groups a module
1031
+ of yours imports (`?forgebook-files`), are the only groups the server reads, whatever an address names. A query that
1032
+ names one file (`?forgebook-code`, `?forgebook-markdown`) follows Vite's `server.fs`, as `?raw` does.
1033
+ - **`server.fs.deny` wins over your globs.** A file it refuses stays out of the catalogue, its search and "Used in", with
1034
+ a warning. A file your globs name outside `server.fs.allow` is listed and served, as Vite serves any file a module of
1035
+ the project imports.
1036
+
1037
+ A command, an agent or `curl` sends no browser headers: `forgebook mcp`, `forgebook stories` and the agent API
1038
+ (`/__forgebook/api/`) work as usual, and anyone who can reach the port can use them. With `--host` that means your whole
1039
+ network, so use it only on one you trust.
1040
+
934
1041
  ## Your look in the shell
935
1042
  The catalogue's page can take your library's colours, fonts and corners. Nothing runs in the browser for it: the theme is
936
1043
  written into the page at build, and the frames are left alone, since they belong to your stories.
@@ -60,6 +60,9 @@ const changedOf = async (config, options, log) => {
60
60
  const body = await askCatalogue(config, options.url, "changed", params, log);
61
61
  if (!isChangedStoriesDocument(body))
62
62
  throw new ScriptError("The catalogue's answer has an unexpected shape");
63
+ // said aloud: an empty list then means the imports were not read, not that the change touches nothing
64
+ for (const problem of body.problems)
65
+ log.warn(problem);
63
66
  return body;
64
67
  };
65
68
  /**
@@ -12,7 +12,7 @@ import type { TokenClash } from "../../src/vite/tokenClashes.ts";
12
12
  */
13
13
  export declare const storiesText: (document: StoriesDocument, style: OutputStyle) => string;
14
14
  /**
15
- * One story's facts: where it is, where it opens, its args and its component's props.
15
+ * One story's facts: where it is, where it opens, its args, its component's props and those every use must set.
16
16
  *
17
17
  * @param document - the story document.
18
18
  * @param style - colours, glyphs and width.
@@ -20,7 +20,7 @@ export declare const storiesText: (document: StoriesDocument, style: OutputStyle
20
20
  export declare const storyText: (document: StoryDocument, style: OutputStyle) => string;
21
21
  /**
22
22
  * A check's report as a table: one row per problem, its story, its view and its kind; a story that passes in a view gets
23
- * one row saying so.
23
+ * one row saying so. A catalogue that checks no accessibility says so first, so "passes" is not read as axe's.
24
24
  *
25
25
  * @param report - the check's report.
26
26
  * @param style - colours, glyphs and width.
@@ -23,7 +23,7 @@ export const storiesText = (document, style) => table(document.components.flatMa
23
23
  `${component.title} › ${story.name}${story.isUnlisted ? " (unlisted)" : ""}`
24
24
  ])), { columns: [{ header: "Id" }, { header: "Story", tone: "muted" }] }, uncut(style));
25
25
  /**
26
- * One story's facts: where it is, where it opens, its args and its component's props.
26
+ * One story's facts: where it is, where it opens, its args, its component's props and those every use must set.
27
27
  *
28
28
  * @param document - the story document.
29
29
  * @param style - colours, glyphs and width.
@@ -37,7 +37,8 @@ export const storyText = (document, style) => {
37
37
  ["Frame", document.story.links.frame],
38
38
  ["Embed", document.story.links.embed],
39
39
  ["Args", JSON.stringify(document.args)],
40
- ["Props", document.docs === undefined ? "none" : document.docs.props.map((prop) => prop.name).join(", ")]
40
+ ["Props", document.docs === undefined ? "none" : document.docs.props.map((prop) => prop.name).join(", ")],
41
+ ["Required", document.requiredProps.length === 0 ? "none" : document.requiredProps.join(", ")]
41
42
  ];
42
43
  if (document.story.description !== undefined)
43
44
  entries.push(["About", document.story.description]);
@@ -63,21 +64,22 @@ const judgementLines = (result) => {
63
64
  };
64
65
  /**
65
66
  * A check's report as a table: one row per problem, its story, its view and its kind; a story that passes in a view gets
66
- * one row saying so.
67
+ * one row saying so. A catalogue that checks no accessibility says so first, so "passes" is not read as axe's.
67
68
  *
68
69
  * @param report - the check's report.
69
70
  * @param style - colours, glyphs and width.
70
71
  */
71
- export const checkText = (report, style) => table(report.results.flatMap((result) => [
72
- ...(result.problems.length === 0
73
- ? [[result.storyId, result.view || "default", "passes"]]
74
- : result.problems.map((problem) => [
75
- result.storyId,
76
- result.view || "default",
77
- `${problem.kind}: ${problem.message}`
78
- ])),
79
- ...judgementLines(result)
80
- ]), { columns: [{ header: "Story" }, { header: "View", tone: "muted" }, { header: "Result" }] }, uncut(style));
72
+ export const checkText = (report, style) => (report.a11y === "off" ? "Accessibility is not checked: the catalogue sets a11y: false.\n" : "") +
73
+ table(report.results.flatMap((result) => [
74
+ ...(result.problems.length === 0
75
+ ? [[result.storyId, result.view || "default", "passes"]]
76
+ : result.problems.map((problem) => [
77
+ result.storyId,
78
+ result.view || "default",
79
+ `${problem.kind}: ${problem.message}`
80
+ ])),
81
+ ...judgementLines(result)
82
+ ]), { columns: [{ header: "Story" }, { header: "View", tone: "muted" }, { header: "Result" }] }, uncut(style));
81
83
  /**
82
84
  * The frames' errors as a table: their number, the story, what went wrong; nothing when there are none.
83
85
  *
@@ -10,6 +10,10 @@ in JSON with `--json`, and every answer about a story carries the links that ope
10
10
  reads it, never MDX or addons). Files with the same title are one component, as in Storybook: each story keeps its
11
11
  own file (`file` in `forgebook stories --json`), and the component lists them all (`files`).
12
12
  - Every named export of a story file is a story; leave helpers and data out with the meta's `excludeStories`.
13
+ - The catalogue lists the files its `stories` globs name in `forgebook.config.ts`; a glob that starts with `!` leaves
14
+ files out (`"!src/**/drafts/**"`), whatever its place in the list. A new story file that `forgebook stories` does not
15
+ list is outside the globs, left out by a `!` one, or refused by Vite's `server.fs.deny` (the dev server warns): put it
16
+ where they list it, or change the globs on purpose.
13
17
  - Story ids are Storybook's (`inputs-readmore--open-by-default`, prefixed by the source's id); the commands also take
14
18
  the ids Forgebook gave before (`inputs-read-more--open-by-default`) and answer with the current one.
15
19
  - The meta is the default export; the stories are named exports, each with a one-line TSDoc saying what it shows (the
@@ -59,8 +63,9 @@ in JSON with `--json`, and every answer about a story carries the links that ope
59
63
  `({ props: args, template: "…" })`. The Code tab writes the story by the component's selector.
60
64
  - In a SvelteKit app (`framework: sveltekit()`), the types come from `@forgebook/sveltekit`, and a story sets the page its
61
65
  components read with `parameters.sveltekit`: `page: { url: "/blog/hello" }` (the route and params are read from
62
- `src/routes`), `page.data` or the page's own `load` (run in the frame, its fetch answered by `parameters.msw`), and
63
- `forms.result` for what an enhanced form's action answers. Links and `goto` move the story's page and are listed among
66
+ `src/routes`), `page.data` or the page's own `load` (run before the story is drawn, its fetch answered by
67
+ `parameters.msw` in the frame and in the server tests, where a fetch of the page's origin that no handler answers
68
+ fails the story), and `forms.result` for what an enhanced form's action answers. Links and `goto` move the story's page and are listed among
64
69
  its events; in a play, `storyPage()` from `@forgebook/sveltekit/page` reads where it went and the calls it made. A
65
70
  component that imports `$app/env/private` or `$app/server` fails in its frame by design: give it what it reads instead.
66
71
  - `parameters.main: true` marks the one real, well-made use drawn at the top of the component's page.
@@ -68,7 +73,8 @@ in JSON with `--json`, and every answer about a story carries the links that ope
68
73
  meaningful state, not one per prop value.
69
74
  - Add a worst case: very long text, words without spaces, huge numbers (`forgebook new` writes one).
70
75
  - To try a component as on a phone, the catalogue's Touch switch (`touch=touch` in a view: `--view "touch=touch"`)
71
- sends the story a finger's events and a phone's hover and pointer media. It is a simulation: native scrolling, the
76
+ sends the story a finger's events and a phone's hover and pointer media; a tap on a text field focuses it and puts the
77
+ caret there. It is a simulation: native scrolling, the
72
78
  virtual keyboard and real Safari still need a device, or `forgebook dev --touch` (Chromium's real touch).
73
79
  A story reads it as `context.globals.touch` (`"mouse"` or `"touch"`) or fixes it with
74
80
  `globals: { touch: "touch" }`. A story meant for a phone writes `parameters.touch: true` (or its meta does): the
@@ -102,8 +108,8 @@ in JSON with `--json`, and every answer about a story carries the links that ope
102
108
  - Texts a person reads come from the source's texts, not from the story: `args: { label: storyText("badge.new") }`
103
109
  (`@forgebook/story-format/texts`), or `useStoryText()` inside a `render`. A source gives each language's table, or a
104
110
  loader of it (`texts: { en: () => import("./locales/en.json") }`), which keeps the texts out of the catalogue's page.
105
- - Write `title`, `parameters.main`, `group`, `height`, `touch`, `logic` and `design.figma` as plain values: the catalogue reads them without running
106
- the file.
111
+ - Write `title`, `parameters.main`, `group`, `height`, `touch`, `logic`, `requiredFirst` and `design.figma` as plain
112
+ values: the catalogue reads them without running the file.
107
113
  - In a Next.js project (`framework: nextjs()`), stories take their types from `@forgebook/nextjs`. A story says where
108
114
  its component is with `parameters.nextjs.navigation` (`pathname`, `query`, `params`; `router` for the Pages Router),
109
115
  and what its server code reads with `nextjs.cookies`, `headers` and `draftMode`. A link or a router call moves the
@@ -117,6 +123,10 @@ in JSON with `--json`, and every answer about a story carries the links that ope
117
123
  - A component with many props names their groups in its TSDoc (`@group Paging`): `forgebook story` returns each
118
124
  prop's `group`, and a prop typed with a documented object (`texts?: FinderTexts`) its `members`, each with its type,
119
125
  default and TSDoc: read them to know every text a component takes before passing one.
126
+ - `forgebook story --json` and `get_story` give `requiredProps` at the top, the names of the component's props every
127
+ use must set (events among them; a subcomponent's say it with their own `isRequired`); `docs.props` stays in the
128
+ order of the source, whatever the catalogue's `requiredFirst` shows a person.
129
+ Set every name in `requiredProps` when you use the component or write its story.
120
130
 
121
131
  ### Docs pages
122
132
 
@@ -154,7 +164,7 @@ leave it out.
154
164
  | `npx forgebook search onValueChange --json` | The whole catalogue by its text: which component has a prop, where the docs say something, with the address of the exact heading or prop |
155
165
  | `npx forgebook new src/Badge.tsx` | A component's first stories, from its typed props (`Badge.vue` or `badge.component.ts` writes `Badge.stories.ts`) |
156
166
  | `npx forgebook stories --component badge --json` | The stories and their ids; fails when a story file is broken |
157
- | `npx forgebook story <id> --json` | One story: its args, its component's props, the components whose stories use it, its design |
167
+ | `npx forgebook story <id> --json` | One story: the props every use must set (`requiredProps`), its args, its component's props, the components whose stories use it, its design |
158
168
  | `npx forgebook pages --json` | The docs pages, with their ids and addresses; fails when a page names what is not there |
159
169
  | `npx forgebook page <id> --json` | One docs page: its Markdown, its headings, the address that opens it |
160
170
  | `npx forgebook check <id> --json` | The catalogue's checks on one story: accessibility, spills, overlays, a modal's focus, errors, a failed play |
@@ -174,6 +184,11 @@ leave it out.
174
184
  Never write a raw colour or size when a token exists: ask `tokens --nearest` first, and use a match only when it says
175
185
  `isClose` or `isExact`. A match that is not close means no token fits: say so, do not force one.
176
186
 
187
+ A catalogue may turn off its accessibility check (`a11y: false`) or its component docs (`componentDocs: false`). Then a
188
+ check's report says `"a11y": "off"` (it ran no axe: a pass says nothing of accessibility), and `story` says
189
+ `"componentDocs": "off"` with no `docs` and no required props: read the component's source for its props, and the
190
+ story's args for what it sets; `changed` finds no story through the imports, so check the stories you touched by name.
191
+
177
192
  A story's frame runs on its own origin (the dev server's second port), unless the catalogue keeps it on its own
178
193
  (`frames: "same-origin"`, `forgebook dev --frames same-origin`): open a story by the `page` or `frame` link an answer
179
194
  gives, never by building a frame's address yourself, and do not expect a story's code to reach the catalogue's page or
@@ -186,7 +186,7 @@ export const catalogueTools = ({ ask, writeNew, check, shot, checkPage, servePre
186
186
  },
187
187
  {
188
188
  name: "get_story",
189
- description: "One story in detail: its args as the catalogue renders it, its component's props (types, defaults, descriptions), the components whose stories use it (usedIn), the device it is shown in by default (device), its design in Figma when it has one (design: the link, the file key and the node id, and the address where a person sees it beside the story), its links in the view asked for, and what failed while reading it.",
189
+ description: "One story in detail: the names of the props every use must set (requiredProps, first), its args as the catalogue renders it, its component's props in the order of the source (types, defaults, descriptions), the components whose stories use it (usedIn), the device it is shown in by default (device), its design in Figma when it has one (design: the link, the file key and the node id, and the address where a person sees it beside the story), its links in the view asked for, and what failed while reading it.",
190
190
  inputSchema: {
191
191
  type: "object",
192
192
  properties: {
@@ -10,13 +10,18 @@ import { findUp } from "@ensiria/cli-kit/files";
10
10
  import { ScriptError } from "@ensiria/cli-kit/script";
11
11
  import { autoTitle, patternBase } from "../../src/vite/autoTitle.js";
12
12
  import { findConfigFile, loadConfigFile } from "../../src/vite/configFile.js";
13
+ import { isNegated } from "../../src/vite/storyPatterns.js";
13
14
  import { VITE_PATHS } from "../../src/vite/vitePaths.js";
14
15
  import { frameworkDocsModule } from "../frameworkDocs.js";
15
16
  import { newStories } from "./newStories.js";
16
- /** The folder the title starts from: the catalogue's first stories pattern's, else `src` for a file under it. */
17
+ /**
18
+ * The folder the title starts from: the catalogue's first stories pattern's (a `!` one lists nothing), else `src` for
19
+ * a file under it.
20
+ */
17
21
  const titleBase = async (packageDir, storiesFile) => {
18
22
  const config = findConfigFile(packageDir);
19
- const [pattern] = config === undefined ? [] : (await loadConfigFile(config)).stories;
23
+ const patterns = config === undefined ? [] : (await loadConfigFile(config)).stories;
24
+ const pattern = patterns.find((each) => !isNegated(each));
20
25
  if (pattern !== undefined)
21
26
  return patternBase(pattern).replace(/^\.\//, "");
22
27
  return VITE_PATHS.relative(packageDir, storiesFile).split("/")[0] === "src" ? "src" : "";
@@ -174,7 +174,7 @@ const collectPages = (packageDir, exports, problems) => {
174
174
  return [];
175
175
  const file = VITE_PATHS.resolve(packageDir, source);
176
176
  const patterns = [...(readLinkedFile(file) ?? "").matchAll(PAGES_IMPORT)].flatMap((match) => match[1] ?? []);
177
- const pages = patterns.flatMap((pattern) => indexedDocPages(VITE_PATHS.dirname(file), pattern, () => undefined));
177
+ const pages = patterns.flatMap((pattern) => indexedDocPages(VITE_PATHS.dirname(file), [pattern], () => undefined));
178
178
  for (const page of pages)
179
179
  for (const note of page.index.notes ?? [])
180
180
  problems.push(`${VITE_PATHS.relative(packageDir, page.file)}: ${note}`);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The props a story's args stand for, for a catalogue that reads no component docs (`componentDocs: false`): the Props
3
+ * tab's controls then come from the story's own args and its `argTypes`, as Storybook's do without a docgen.
4
+ *
5
+ * @remarks
6
+ * An arg's kind is read from its value: a text, a number, a toggle, an event for an arg the frame records as an
7
+ * action. A list or a value the source names (`namedValues`) is listed for reading, unless its type is one the source
8
+ * names values for. Nothing is said of a prop the story gives no arg: without the types there is no way to know it.
9
+ * The story's `argTypes` are laid over these as over the types' props (`argTypes.ts`). A shared link's values are
10
+ * checked against these kinds (`addressArgs.ts`): without the types, a text arg takes any text, so a prop of a few
11
+ * values keeps a link to them only through its `argTypes` options. Pure.
12
+ *
13
+ * @module
14
+ */
15
+ import type { ComponentDocs, PropDocs } from "./componentDocs.ts";
16
+ import type { PanelArgs } from "./messages.ts";
17
+ /**
18
+ * The props a story's args stand for, in the args' order, then its actions the args do not hold.
19
+ *
20
+ * @param args - the story's args, as its frame reported them.
21
+ * @param actions - the args the frame records as actions.
22
+ */
23
+ export declare const propsOfArgs: (args: PanelArgs, actions: readonly string[]) => readonly PropDocs[];
24
+ /**
25
+ * A component's docs made from a story's args: its name, and the props its args stand for; nothing else is known
26
+ * without its types.
27
+ *
28
+ * @param name - the component's name.
29
+ * @param args - the story's args, as its frame reported them.
30
+ * @param actions - the args the frame records as actions.
31
+ */
32
+ export declare const docsOfArgs: (name: string, args: PanelArgs, actions: readonly string[]) => ComponentDocs;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The props a story's args stand for, for a catalogue that reads no component docs (`componentDocs: false`): the Props
3
+ * tab's controls then come from the story's own args and its `argTypes`, as Storybook's do without a docgen.
4
+ *
5
+ * @remarks
6
+ * An arg's kind is read from its value: a text, a number, a toggle, an event for an arg the frame records as an
7
+ * action. A list or a value the source names (`namedValues`) is listed for reading, unless its type is one the source
8
+ * names values for. Nothing is said of a prop the story gives no arg: without the types there is no way to know it.
9
+ * The story's `argTypes` are laid over these as over the types' props (`argTypes.ts`). A shared link's values are
10
+ * checked against these kinds (`addressArgs.ts`): without the types, a text arg takes any text, so a prop of a few
11
+ * values keeps a link to them only through its `argTypes` options. Pure.
12
+ *
13
+ * @module
14
+ */
15
+ /**
16
+ * The prop an arg stands for, its kind and type read from its value.
17
+ *
18
+ * @param name - the arg's name.
19
+ * @param value - its value, as the frame reported it.
20
+ * @param isAction - whether the frame records it as an action.
21
+ */
22
+ const propOfArg = (name, value, isAction) => {
23
+ const prop = { name, description: "", isRequired: false };
24
+ if (isAction)
25
+ return { ...prop, kind: "event", type: "function" };
26
+ if (typeof value === "string")
27
+ return { ...prop, kind: "text", type: "string" };
28
+ if (typeof value === "number")
29
+ return { ...prop, kind: "number", type: "number" };
30
+ if (typeof value === "boolean")
31
+ return { ...prop, kind: "toggle", type: "boolean" };
32
+ if (value === null)
33
+ return { ...prop, kind: "other", type: "null" };
34
+ if (Array.isArray(value))
35
+ return { ...prop, kind: "other", type: "array" };
36
+ // a value the source names: its type, so the panel offers the source's values for it
37
+ return { ...prop, kind: "other", type: "of" in value ? value.of : "unknown" };
38
+ };
39
+ /**
40
+ * The props a story's args stand for, in the args' order, then its actions the args do not hold.
41
+ *
42
+ * @param args - the story's args, as its frame reported them.
43
+ * @param actions - the args the frame records as actions.
44
+ */
45
+ export const propsOfArgs = (args, actions) => {
46
+ const recorded = new Set(actions);
47
+ const fromArgs = Object.entries(args).map(([name, value]) => propOfArg(name, value, recorded.has(name)));
48
+ const named = new Set(Object.keys(args));
49
+ const fromActions = actions
50
+ .filter((name) => !named.has(name))
51
+ .map((name) => ({ name, kind: "event", type: "function", description: "", isRequired: false }));
52
+ return [...fromArgs, ...fromActions];
53
+ };
54
+ /**
55
+ * A component's docs made from a story's args: its name, and the props its args stand for; nothing else is known
56
+ * without its types.
57
+ *
58
+ * @param name - the component's name.
59
+ * @param args - the story's args, as its frame reported them.
60
+ * @param actions - the args the frame records as actions.
61
+ */
62
+ export const docsOfArgs = (name, args, actions) => ({
63
+ name,
64
+ description: "",
65
+ summary: "",
66
+ props: propsOfArgs(args, actions),
67
+ parts: [],
68
+ subcomponents: []
69
+ });
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @module
13
13
  */
14
- import type { StoryDeviceSetting } from "@forgebook/story-format/csf";
14
+ import type { RequiredFirst, StoryDeviceSetting } from "@forgebook/story-format/csf";
15
15
  import { type AudienceId } from "./audiences.ts";
16
16
  import { type Backdrop, type CustomBackdrop } from "./backdrops.ts";
17
17
  import { type ControlKeys } from "./controlKeys.ts";
@@ -84,8 +84,11 @@ export type CatalogueLists = {
84
84
  readonly storyGroups?: readonly StoryGroupSetting[];
85
85
  /** The panel's tabs turned off, Forgebook's or a plugin's (`{ a11y: false }`). */
86
86
  readonly tabs?: Readonly<Partial<Record<PanelTabId, false>> & Record<string, false>>;
87
- /** The accessibility rules. */
88
- readonly a11y?: Partial<A11ySettings>;
87
+ /**
88
+ * The accessibility rules, or `false` to check nothing: no Accessibility tab or column, axe's script neither in a
89
+ * build nor offered to the frames, and the generated tests and `forgebook check` leave axe out.
90
+ */
91
+ readonly a11y?: false | Partial<A11ySettings>;
89
92
  /** The measuring grid's step, in CSS px. */
90
93
  readonly gridStep?: number;
91
94
  /** Texts of the shell replaced, by their name in `STRINGS` or `LATER_STRINGS` (a translation, another word). */
@@ -108,6 +111,13 @@ export type CatalogueLists = {
108
111
  * `false` builds nothing for it (no import graph is read) and leaves it out of the agents' answers.
109
112
  */
110
113
  readonly usedIn?: boolean;
114
+ /**
115
+ * Whether each component's docs are read from its types and TSDoc (its props, events, description, guidance and
116
+ * signature); `true` by default. `false` starts no TypeScript program: no API reference, the Props tab's controls
117
+ * come from the story's args and `argTypes`, and "Used in", which reads the import graph with the same programs, is
118
+ * off too.
119
+ */
120
+ readonly componentDocs?: boolean;
111
121
  /**
112
122
  * Who a component's page is read by before a person picks: `designer`, `coder` or `tester` reorder and filter the
113
123
  * page for them, and `everyone` (the default) draws all of it. The address's `audience` wins, so a link keeps its view;
@@ -143,6 +153,13 @@ export type CatalogueLists = {
143
153
  * groups named nowhere follow in the order they first appear. The props that name no group always come first.
144
154
  */
145
155
  readonly propGroups?: readonly string[];
156
+ /**
157
+ * Where every component's API reference and Props tab list the required props: first inside each group
158
+ * (`"in-groups"`, the default), all in one "Required" section before the groups (`"section"`), or in the order of
159
+ * the source (`false`). A meta's or a story's `parameters.requiredFirst` wins over it; a reader's "Required first"
160
+ * switch turns the ordering on or off over both.
161
+ */
162
+ readonly requiredFirst?: RequiredFirst;
146
163
  };
147
164
  /** The shell's own appearance: light, dark, the system's, or the colour scheme of the story shown. */
148
165
  export type Appearance = "light" | "dark" | "system" | "story";
@@ -173,8 +190,13 @@ export type CatalogueSettings = {
173
190
  readonly storyGroups: readonly StoryGroupSetting[];
174
191
  /** The panel's tabs shown, Forgebook's then the plugins', by id. */
175
192
  readonly tabs: readonly string[];
176
- /** The accessibility rules. */
177
- readonly a11y: A11ySettings;
193
+ /**
194
+ * The accessibility rules the Accessibility tab and column, the frames and the tests check; none when it checks
195
+ * nothing (`a11y: false`).
196
+ */
197
+ readonly a11y: A11ySettings | undefined;
198
+ /** Whether each component's docs are read from its types. */
199
+ readonly hasComponentDocs: boolean;
178
200
  /** The measuring grid's step, in CSS px. */
179
201
  readonly gridStep: number;
180
202
  /** The shell's texts replaced, by name. */
@@ -202,9 +224,30 @@ export type CatalogueSettings = {
202
224
  readonly device: DeviceDefault;
203
225
  /** The prop groups listed first, in their order; none by default. */
204
226
  readonly propGroups: readonly string[];
227
+ /** Where the required props are listed when a component or a story does not say; `"in-groups"` by default. */
228
+ readonly requiredFirst: RequiredFirst;
205
229
  };
206
230
  /** WCAG 2.2 at levels A and AA: the rules every story passes by default. */
207
231
  export declare const DEFAULT_A11Y_TAGS: readonly string[];
232
+ /**
233
+ * Whether a catalogue checks accessibility: unless its lists say `a11y: false`.
234
+ *
235
+ * @param lists - the lists its configuration sets.
236
+ */
237
+ export declare const checksA11y: (lists: CatalogueLists) => boolean;
238
+ /**
239
+ * Whether a catalogue reads its components' docs from their types: unless its lists say `componentDocs: false`.
240
+ *
241
+ * @param lists - the lists its configuration sets.
242
+ */
243
+ export declare const readsComponentDocs: (lists: CatalogueLists) => boolean;
244
+ /**
245
+ * Whether a catalogue offers "Used in": unless its lists turn it off, or turn off the component docs, whose
246
+ * TypeScript programs read the import graph it is joined from.
247
+ *
248
+ * @param lists - the lists its configuration sets.
249
+ */
250
+ export declare const offersUsedIn: (lists: CatalogueLists) => boolean;
208
251
  /**
209
252
  * A catalogue's settings, its lists' defaults filled in.
210
253
  *