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
@@ -0,0 +1,11 @@
1
+ import type { PanelArgs } from "../messages.ts";
2
+ import { type DocsState } from "./useComponentDocs.ts";
3
+ /**
4
+ * The panel's docs of the shown story.
5
+ *
6
+ * @param loadDocs - loads the component's docs; `undefined` when the source has none for the file.
7
+ * @param name - the component's name.
8
+ * @param args - the story's args, as its frame reported them; none before it did.
9
+ * @param actions - the args the frame records as actions.
10
+ */
11
+ export declare const usePanelDocs: (loadDocs: (() => Promise<unknown>) | undefined, name: string, args: PanelArgs | undefined, actions: readonly string[]) => DocsState;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The docs the story's panel reads: the component's, read from its types, or for a catalogue that reads none
3
+ * (`componentDocs: false`) the story's args as its props (`argsProps.ts`), once its frame reported them.
4
+ *
5
+ * @module
6
+ */
7
+ import { useMemo } from "react";
8
+ import { docsOfArgs } from "../argsProps.js";
9
+ import { useCatalogue } from "./catalogueContext.js";
10
+ import { useComponentDocs } from "./useComponentDocs.js";
11
+ /** The state before a story's frame reported its args: one value, so what reads it is not renewed each render. */
12
+ const DOCS_LOADING = { status: "loading" };
13
+ /**
14
+ * The panel's docs of the shown story.
15
+ *
16
+ * @param loadDocs - loads the component's docs; `undefined` when the source has none for the file.
17
+ * @param name - the component's name.
18
+ * @param args - the story's args, as its frame reported them; none before it did.
19
+ * @param actions - the args the frame records as actions.
20
+ */
21
+ export const usePanelDocs = (loadDocs, name, args, actions) => {
22
+ const { hasComponentDocs } = useCatalogue();
23
+ const typeDocs = useComponentDocs(loadDocs);
24
+ return useMemo(() => {
25
+ if (hasComponentDocs)
26
+ return typeDocs;
27
+ return args === undefined ? DOCS_LOADING : { status: "ready", value: docsOfArgs(name, args, actions) };
28
+ }, [hasComponentDocs, typeDocs, name, args, actions]);
29
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The reader's "Required first" switch, kept in this browser for the whole catalogue, and the layout of the required
3
+ * props it leaves for a component or a story (`requiredFirstLayout`).
4
+ *
5
+ * @remarks
6
+ * Stored as `useStored` stores a reader's preference (under the catalogue's id), but shared live by every table that
7
+ * shows it: a docs page may hold several API references, and turning the switch in one turns it in all. Storage may
8
+ * be refused (a private window): the switch then holds for the page only, and nothing fails.
9
+ *
10
+ * @module
11
+ */
12
+ import type { RequiredFirst } from "@forgebook/story-format/csf";
13
+ /** Where the required props go, and the reader's switch over it. */
14
+ export type RequiredFirstState = {
15
+ /** The layout to draw. */
16
+ readonly layout: RequiredFirst;
17
+ /** Whether the switch shows on: the layout puts the required props first. */
18
+ readonly isOn: boolean;
19
+ /** Turns the switch, for every component, and keeps it in this browser. */
20
+ readonly setIsOn: (isOn: boolean) => void;
21
+ };
22
+ /**
23
+ * Where a component's or a story's required props go for this reader.
24
+ *
25
+ * @param own - the story's or the component's own setting (`parameters.requiredFirst`); none for the catalogue's.
26
+ */
27
+ export declare const useRequiredFirst: (own: RequiredFirst | undefined) => RequiredFirstState;
@@ -0,0 +1,67 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { requiredFirstLayout } from "../propGroups.js";
3
+ import { useCatalogue } from "./catalogueContext.js";
4
+ import { readStored, writeStored } from "./useStored.js";
5
+ /**
6
+ * The reader's switch from the text it is stored as: none for anything else, a text another script wrote included.
7
+ *
8
+ * @param stored - the stored text; none when nothing is stored.
9
+ */
10
+ const readerSwitch = (stored) => {
11
+ if (stored === "on")
12
+ return true;
13
+ return stored === "off" ? false : undefined;
14
+ };
15
+ /** The tables showing the switch, told when it turns. */
16
+ const listeners = new Set();
17
+ /**
18
+ * The switch as turned in this page, by key: it wins over storage, which a browser may refuse to write (a private
19
+ * window, a full quota) while still reading an older value.
20
+ */
21
+ const turnedInPage = new Map();
22
+ /**
23
+ * Listens to the switch: turned in this page, or in another of the catalogue's tabs.
24
+ *
25
+ * @param listener - called when it may have turned.
26
+ * @returns what stops listening.
27
+ */
28
+ const subscribe = (listener) => {
29
+ // another tab's choice is the newer one: this page's own gives way to it
30
+ const onStorage = (event) => {
31
+ if (event.key === null)
32
+ turnedInPage.clear();
33
+ else
34
+ turnedInPage.delete(event.key);
35
+ listener();
36
+ };
37
+ listeners.add(listener);
38
+ window.addEventListener("storage", onStorage);
39
+ return () => {
40
+ listeners.delete(listener);
41
+ window.removeEventListener("storage", onStorage);
42
+ };
43
+ };
44
+ /** No stored switch where there is no page to store it in. */
45
+ const NOT_STORED = () => null;
46
+ /**
47
+ * Where a component's or a story's required props go for this reader.
48
+ *
49
+ * @param own - the story's or the component's own setting (`parameters.requiredFirst`); none for the catalogue's.
50
+ */
51
+ export const useRequiredFirst = (own) => {
52
+ const catalogue = useCatalogue();
53
+ const key = `${catalogue.id}:required-first`;
54
+ const stored = useSyncExternalStore(subscribe, () => turnedInPage.get(key) ?? readStored(key), NOT_STORED);
55
+ const layout = requiredFirstLayout(own, catalogue.requiredFirst, readerSwitch(stored));
56
+ return {
57
+ layout,
58
+ isOn: layout !== false,
59
+ setIsOn: (isOn) => {
60
+ const text = isOn ? "on" : "off";
61
+ turnedInPage.set(key, text);
62
+ writeStored(key, text);
63
+ for (const listener of listeners)
64
+ listener();
65
+ }
66
+ };
67
+ };
@@ -95,6 +95,8 @@ export type StoryCheckReport = {
95
95
  readonly schema: 1;
96
96
  /** One result per story and view. */
97
97
  readonly results: readonly StoryCheckResult[];
98
+ /** `"off"` when the catalogue checks no accessibility (`a11y: false`): no result ran axe. Absent when it does. */
99
+ readonly a11y?: "off";
98
100
  };
99
101
  /**
100
102
  * Whether what the testing package answered is a check's report.
@@ -38,7 +38,8 @@ export const isStoryCheckReport = (value) => typeof value === "object" &&
38
38
  value.schema === 1 &&
39
39
  "results" in value &&
40
40
  Array.isArray(value.results) &&
41
- value.results.every(isResult);
41
+ value.results.every(isResult) &&
42
+ (!("a11y" in value) || value.a11y === undefined || value.a11y === "off");
42
43
  /**
43
44
  * Whether what the testing package answered is a comparison's report.
44
45
  *
@@ -76,7 +76,8 @@ export const fixedViewOf = (globals, parameters, catalogue) => {
76
76
  */
77
77
  export const withFixed = (view, fixed) => ({
78
78
  ...view,
79
- switches: { ...view.switches, ...fixed.switches },
79
+ // the same object when the story fixes no switch: a new one would tell the frames of a view that did not change
80
+ switches: Object.keys(fixed.switches).length === 0 ? view.switches : { ...view.switches, ...fixed.switches },
80
81
  ...(fixed.backdrop === undefined ? {} : { backdrop: fixed.backdrop }),
81
82
  ...(fixed.width === undefined || view.width === undefined ? {} : { width: fixed.width })
82
83
  });
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @module
15
15
  */
16
- import type { StoryDeviceSetting, StoryGroup } from "@forgebook/story-format/csf";
16
+ import type { RequiredFirst, StoryDeviceSetting, StoryGroup } from "@forgebook/story-format/csf";
17
17
  import type { FigmaLink, StoryBackground, StoryControlsSetting, StorySource } from "@forgebook/story-format/source";
18
18
  import { type TreePage } from "./pageTree.ts";
19
19
  /** One story in the tree. */
@@ -44,6 +44,11 @@ export type TreeStory = {
44
44
  readonly device: StoryDeviceSetting | undefined;
45
45
  /** Its design (`parameters.design.figma`): its own, else the meta's; none by default, nor when its own was refused. */
46
46
  readonly design: FigmaLink | undefined;
47
+ /**
48
+ * Where its Props tab lists the required props (`parameters.requiredFirst`): its own, else its file's meta's; none
49
+ * when neither sets one, for the catalogue's.
50
+ */
51
+ readonly requiredFirst: RequiredFirst | undefined;
47
52
  /** Its tags, the meta's and its own (`!dev` keeps it out of the lists, `!test` out of the generated tests). */
48
53
  readonly tags: readonly string[];
49
54
  /** Whether a `play` function acts on it. */
@@ -87,6 +92,11 @@ export type TreeComponent = {
87
92
  readonly design: FigmaLink | undefined;
88
93
  /** The prop groups its meta lists first (`parameters.propGroups`), in their order; empty when none. */
89
94
  readonly propGroups: readonly string[];
95
+ /**
96
+ * Where its page lists the required props (its first file's meta's `parameters.requiredFirst`); none when it sets
97
+ * none, for the catalogue's.
98
+ */
99
+ readonly requiredFirst: RequiredFirst | undefined;
90
100
  };
91
101
  /** A group's entry for a component that lives elsewhere and is listed here too (the meta's `alsoIn`). */
92
102
  export type TreeAlias = {
@@ -176,6 +176,7 @@ const treeStory = (id, file, entry, story, design) => ({
176
176
  ? undefined
177
177
  : { ...entry.device, ...story.device },
178
178
  design,
179
+ requiredFirst: story.requiredFirst ?? entry.requiredFirst,
179
180
  tags: story.tags ?? [],
180
181
  hasPlay: story.hasPlay === true
181
182
  });
@@ -247,7 +248,8 @@ const sourceFamilies = (source, problems) => {
247
248
  keywords: metaList(component, "keywords"),
248
249
  design: componentDesign(component),
249
250
  // its first file's, as its docs are
250
- propGroups: component.files[0]?.entry.propGroups ?? []
251
+ propGroups: component.files[0]?.entry.propGroups ?? [],
252
+ requiredFirst: component.files[0]?.entry.requiredFirst
251
253
  });
252
254
  }
253
255
  placeAliases(source, placed, families, problems);
@@ -162,6 +162,13 @@ export declare const frameAddress: (frameUrl: string, id: string, view: ViewStat
162
162
  * @param view - the view.
163
163
  */
164
164
  export declare const liveView: (view: ViewState) => LiveView;
165
+ /**
166
+ * Whether two live views show the same: their backdrop, text size and each switch's value, whatever the objects.
167
+ *
168
+ * @param left - a live view.
169
+ * @param right - the other.
170
+ */
171
+ export declare const isSameLiveView: (left: LiveView, right: LiveView) => boolean;
165
172
  /**
166
173
  * The scheme a view draws in: its colour scheme switch's, light when the catalogue has none.
167
174
  *
@@ -255,6 +255,19 @@ export const liveView = (view) => ({
255
255
  backdrop: view.backdrop,
256
256
  text: view.text
257
257
  });
258
+ /**
259
+ * Whether two live views show the same: their backdrop, text size and each switch's value, whatever the objects.
260
+ *
261
+ * @param left - a live view.
262
+ * @param right - the other.
263
+ */
264
+ export const isSameLiveView = (left, right) => {
265
+ const leftIds = Object.keys(left.switches);
266
+ return (left.backdrop === right.backdrop &&
267
+ left.text === right.text &&
268
+ leftIds.length === Object.keys(right.switches).length &&
269
+ leftIds.every((id) => left.switches[id] === right.switches[id]));
270
+ };
258
271
  /**
259
272
  * The scheme a view draws in: its colour scheme switch's, light when the catalogue has none.
260
273
  *
@@ -145,6 +145,13 @@ export type StoriesDocument = {
145
145
  export type StoryDocument = {
146
146
  /** The shape's version. */
147
147
  readonly schema: AgentSchema;
148
+ /**
149
+ * The names of the component's required props (`isRequired`), events among them, in the order of the source: what
150
+ * every use must set. Its subcomponents' are not listed: their own `isRequired` says it. First in the answer, so a client that cuts a long one keeps it; `docs` keeps the source's
151
+ * order, whatever the catalogue's `requiredFirst` says, since that setting shapes the pages only. Empty when there
152
+ * are no docs.
153
+ */
154
+ readonly requiredProps: readonly string[];
148
155
  /** The story, with its links in the view asked for. */
149
156
  readonly story: AgentStory;
150
157
  /** Its component's id and title, and its "Used in" when the catalogue offers it. */
@@ -160,6 +167,11 @@ export type StoryDocument = {
160
167
  readonly args: Readonly<Record<string, unknown>>;
161
168
  /** What went wrong while reading it: its module failing to load, its docs failing to be read. */
162
169
  readonly problems: readonly string[];
170
+ /**
171
+ * `"off"` when the catalogue reads no component docs (`componentDocs: false`): `docs` is then none and
172
+ * `requiredProps` empty whatever the component declares, and there is no "Used in". Absent when it reads them.
173
+ */
174
+ readonly componentDocs?: "off";
163
175
  };
164
176
  /** Where a docs page opens, in the view the answer describes. */
165
177
  export type PageLinks = {
@@ -186,6 +186,7 @@ export const storyDocument = (location, parts, pages, view, usage) => {
186
186
  problems.push(problem);
187
187
  return {
188
188
  schema: 1,
189
+ requiredProps: (docs?.props ?? []).filter((prop) => prop.isRequired).map((prop) => prop.name),
189
190
  story,
190
191
  component: {
191
192
  id: component.id,
@@ -250,7 +251,11 @@ export const isStoriesDocument = (value) => isRecord(value) &&
250
251
  *
251
252
  * @param value - the answer's body.
252
253
  */
253
- export const isStoryDocument = (value) => isRecord(value) && value["schema"] === 1 && isRecord(value["story"]) && Array.isArray(value["problems"]);
254
+ export const isStoryDocument = (value) => isRecord(value) &&
255
+ value["schema"] === 1 &&
256
+ Array.isArray(value["requiredProps"]) &&
257
+ isRecord(value["story"]) &&
258
+ Array.isArray(value["problems"]);
254
259
  /**
255
260
  * Whether a value is the tokens document, as a client receives it over HTTP.
256
261
  *
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { realpath } from "node:fs/promises";
19
19
  import { createIdResolver } from "vite";
20
+ import { offersUsedIn, readsComponentDocs } from "../catalogue.js";
20
21
  import { mergedPreviews, NO_PREVIEW, readPreview } from "../frame/storyModule.js";
21
22
  import { sourceFiles } from "../sourceFiles.js";
22
23
  import { findComponentByTitle, findStory, storyTree } from "../storyTree.js";
@@ -144,7 +145,7 @@ const errorsAnswer = (context, live) => {
144
145
  */
145
146
  export const storiesAnswer = async (context) => {
146
147
  const { config, params, tree, pages, view, loaded } = context;
147
- const usage = config.lists.usedIn !== false && params.get("usedIn") === "true"
148
+ const usage = offersUsedIn(config.lists) && params.get("usedIn") === "true"
148
149
  ? await usageBySource(loaded.sources)
149
150
  : undefined;
150
151
  return { status: 200, body: storiesDocument(config.title, tree, pages, view, usage) };
@@ -174,7 +175,15 @@ const changedAnswer = async ({ config, loaded, tree, params, pages, view }) => {
174
175
  ? []
175
176
  : [{ id: found.id, title: story.title, links: storyLinks(pages, found.id, view) }];
176
177
  }));
177
- const body = { schema: 1, since, files, stories, problems: [] };
178
+ // without the import graph no story is found, which must not read as a change that touches none
179
+ const problems = offersUsedIn(config.lists)
180
+ ? []
181
+ : [
182
+ readsComponentDocs(config.lists)
183
+ ? "No story is found through the imports: the catalogue turns Used in off (usedIn: false), whose import graph this reads"
184
+ : "No story is found through the imports: the catalogue reads no component docs (componentDocs: false), whose programs read the import graph"
185
+ ];
186
+ const body = { schema: 1, since, files, stories, problems };
178
187
  return { status: 200, body };
179
188
  };
180
189
  /**
@@ -197,10 +206,15 @@ export const storyAnswer = async (context) => {
197
206
  docs: await settled(files.docs[file]),
198
207
  preview: loaded.preview
199
208
  };
200
- const index = config.lists.usedIn === false ? undefined : await sourceUsage(source);
209
+ const index = offersUsedIn(config.lists) ? await sourceUsage(source) : undefined;
201
210
  // a usage that cannot be read leaves "Used in" out, never the story
202
211
  const usage = index === undefined || typeof index === "string" ? undefined : { tree, index };
203
- return { status: 200, body: storyDocument(location, parts, pages, view, usage) };
212
+ const document = storyDocument(location, parts, pages, view, usage);
213
+ // an agent must tell docs turned off from a story file that names no component
214
+ const body = readsComponentDocs(config.lists)
215
+ ? document
216
+ : { ...document, componentDocs: "off" };
217
+ return { status: 200, body };
204
218
  };
205
219
  /**
206
220
  * Answers one of the API's paths.
@@ -33,8 +33,8 @@ export type FileTexts = readonly [docs: unknown, code: KeptCode, page: Component
33
33
  export declare const textsModuleCode: (texts: readonly FileTexts[], emit: EmitAsset) => string;
34
34
  /** What the plugin gives a build's texts: the docs as the panel reads them, and what reading a page needs. */
35
35
  export type TextSources = Pick<TextModuleContext, "watch" | "resolveImport" | "componentFiles"> & {
36
- /** A story file's component docs as the panel reads them, or `{ error }`. */
37
- readonly docsOf: (storyFile: string) => Promise<unknown>;
36
+ /** A story file's component docs as the panel reads them, or `{ error }`; none when the catalogue reads none. */
37
+ readonly docsOf: ((storyFile: string) => Promise<unknown>) | undefined;
38
38
  };
39
39
  /** The module a build's texts are answered as. */
40
40
  type TextsModule = {
@@ -63,7 +63,7 @@ const textWriter = (emit) => {
63
63
  * @param read - reads a file's texts.
64
64
  * @returns each file's texts, in the files' order.
65
65
  */
66
- const readTexts = (files, read) => Promise.all(files.map(({ file }) => Promise.all([read.docsOf(file), read.codeOf(file), read.pageOf(file)])));
66
+ const readTexts = (files, read) => Promise.all(files.map(({ file }) => Promise.all([read.docsOf?.(file), read.codeOf(file), read.pageOf(file)])));
67
67
  /**
68
68
  * The code of a group's texts module: the functions its loaders call, then, as its default, each file's loaders of its
69
69
  * docs, code and page, in the order of the group's rows.
@@ -74,7 +74,8 @@ const readTexts = (files, read) => Promise.all(files.map(({ file }) => Promise.a
74
74
  export const textsModuleCode = (texts, emit) => {
75
75
  // written in the files' order once all are read, so the names in the module do not depend on which read ended first
76
76
  const writer = textWriter(emit);
77
- const rows = texts.map((each) => `[${each.map((text) => writer.loaderOf(text)).join(", ")}]`);
77
+ // a text not read (the docs of a catalogue that reads none) has no loader, and its place stays in the row
78
+ const rows = texts.map((each) => `[${each.map((text) => (text === undefined ? "null" : writer.loaderOf(text))).join(", ")}]`);
78
79
  return [packedLoaderMaker(PACKED), ...writer.written(), `export default [${rows.join(",\n")}];`].join("\n");
79
80
  };
80
81
  /**
@@ -99,8 +100,8 @@ const textReaders = async (sources) => {
99
100
  * @param group - the group (`storyFilesGroupText`).
100
101
  */
101
102
  const groupKey = (group) => {
102
- const { root, pattern } = storyFilesGroupOf(group);
103
- return storyFilesGroupText(VITE_PATHS.normalize(root), pattern);
103
+ const { root, patterns } = storyFilesGroupOf(group);
104
+ return storyFilesGroupText(VITE_PATHS.normalize(root), patterns);
104
105
  };
105
106
  /**
106
107
  * A build's texts, by group.
@@ -118,8 +119,8 @@ export const groupTexts = () => {
118
119
  const reading = started.get(key);
119
120
  started.delete(key);
120
121
  if (reading === undefined) {
121
- const { root, pattern } = storyFilesGroupOf(group);
122
- return textsModuleCode(await read(indexedStoryFiles(root, pattern, sources.watch), sources), emit);
122
+ const { root, patterns } = storyFilesGroupOf(group);
123
+ return textsModuleCode(await read(indexedStoryFiles(root, patterns, sources.watch), sources), emit);
123
124
  }
124
125
  const texts = await reading.texts;
125
126
  for (const file of reading.watched)
@@ -109,8 +109,9 @@ export declare const findConfigFile: (dir: string) => string | undefined;
109
109
  /**
110
110
  * The lists of the presets and the catalogue, in that order, merged: a single value (a width range, the grid's step)
111
111
  * the last that gives one; the backgrounds, the story groups and the texts entry by entry, a later one replacing an
112
- * earlier one with the same id or name; the tabs, the controls and the rules turned off by any; the toolbar and the
113
- * prop groups' order the last that names one; the device field by field.
112
+ * earlier one with the same id or name; the tabs, the controls and the rules turned off by any, and the accessibility
113
+ * check too (`a11y: false`); the toolbar, the prop groups' order, where the required props go and whether the
114
+ * component docs are read the last that names one; the device field by field.
114
115
  *
115
116
  * @param all - the presets' lists, then the catalogue's.
116
117
  */
@@ -23,10 +23,12 @@ import { dirname, resolve } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
24
  import { AUDIENCE_IDS } from "../audiences.js";
25
25
  import { deviceSettingProblem } from "../deviceView.js";
26
+ import { REQUIRED_FIRST_LAYOUTS } from "../propGroups.js";
26
27
  import { frameThemeCss, mergedThemes, shellThemeCss, shellThemeProblems, themeValueProblem } from "../shellTheme.js";
27
28
  import { switchesProblem } from "../switches.js";
28
29
  import { framesProblem, resolvedFrames } from "./framesConfig.js";
29
30
  import { pluginsProblem, withPluginSwitches } from "./pluginConfig.js";
31
+ import { patternsProblem } from "./storyPatterns.js";
30
32
  import { tokenThemeOf } from "./themeFromTokens.js";
31
33
  import { VITE_PATHS } from "./vitePaths.js";
32
34
  /** The names a configuration file may have, in the order they are looked for. */
@@ -96,15 +98,18 @@ const singleValues = (lists) => ({
96
98
  ...(lists.gridStep === undefined ? {} : { gridStep: lists.gridStep }),
97
99
  ...(lists.toolbar === undefined ? {} : { toolbar: lists.toolbar }),
98
100
  ...(lists.usedIn === undefined ? {} : { usedIn: lists.usedIn }),
101
+ ...(lists.componentDocs === undefined ? {} : { componentDocs: lists.componentDocs }),
99
102
  ...(lists.audience === undefined ? {} : { audience: lists.audience }),
100
103
  ...(lists.appearance === undefined ? {} : { appearance: lists.appearance }),
101
- ...(lists.propGroups === undefined ? {} : { propGroups: lists.propGroups })
104
+ ...(lists.propGroups === undefined ? {} : { propGroups: lists.propGroups }),
105
+ ...(lists.requiredFirst === undefined ? {} : { requiredFirst: lists.requiredFirst })
102
106
  });
103
107
  /**
104
108
  * The lists of the presets and the catalogue, in that order, merged: a single value (a width range, the grid's step)
105
109
  * the last that gives one; the backgrounds, the story groups and the texts entry by entry, a later one replacing an
106
- * earlier one with the same id or name; the tabs, the controls and the rules turned off by any; the toolbar and the
107
- * prop groups' order the last that names one; the device field by field.
110
+ * earlier one with the same id or name; the tabs, the controls and the rules turned off by any, and the accessibility
111
+ * check too (`a11y: false`); the toolbar, the prop groups' order, where the required props go and whether the
112
+ * component docs are read the last that names one; the device field by field.
108
113
  *
109
114
  * @param all - the presets' lists, then the catalogue's.
110
115
  */
@@ -126,7 +131,8 @@ export const mergedLists = (all) => {
126
131
  merged = { ...merged, ...singleValues(lists) };
127
132
  }
128
133
  const builtIn = all.findLast((lists) => lists.backgrounds?.builtIn !== undefined)?.backgrounds?.builtIn;
129
- const tags = all.findLast((lists) => lists.a11y?.tags !== undefined)?.a11y?.tags;
134
+ const rules = all.flatMap((lists) => lists.a11y === undefined || lists.a11y === false ? [] : [lists.a11y]);
135
+ const tags = rules.findLast((each) => each.tags !== undefined)?.tags;
130
136
  return {
131
137
  ...merged,
132
138
  ...(device === undefined ? {} : { device }),
@@ -137,10 +143,13 @@ export const mergedLists = (all) => {
137
143
  storyGroups: mergedById(all.map((lists) => lists.storyGroups)),
138
144
  tabs,
139
145
  controls,
140
- a11y: {
141
- ...(tags === undefined ? {} : { tags }),
142
- disabledRules: [...new Set(all.flatMap((lists) => lists.a11y?.disabledRules ?? []))]
143
- },
146
+ // a check one list turns off stays off, as a tab does: a preset's library may have no use for it
147
+ a11y: all.some((lists) => lists.a11y === false)
148
+ ? false
149
+ : {
150
+ ...(tags === undefined ? {} : { tags }),
151
+ disabledRules: [...new Set(rules.flatMap((each) => each.disabledRules ?? []))]
152
+ },
144
153
  strings,
145
154
  keys,
146
155
  // a later list's view of the same name replaces an earlier one's, where it was
@@ -152,6 +161,18 @@ const isListed = (config, key) => {
152
161
  const list = Reflect.get(config, key);
153
162
  return Array.isArray(list) && list.length > 0;
154
163
  };
164
+ /**
165
+ * Why a configuration's list of globs lists nothing by construction (only `!` patterns), or none.
166
+ *
167
+ * @param config - what the file exports by default.
168
+ * @param key - the list's key.
169
+ */
170
+ const globsProblem = (config, key) => {
171
+ const list = Reflect.get(config, key);
172
+ return Array.isArray(list)
173
+ ? patternsProblem(list.filter((item) => typeof item === "string"))
174
+ : undefined;
175
+ };
155
176
  /**
156
177
  * Why a loaded configuration cannot be used, or none.
157
178
  *
@@ -171,6 +192,11 @@ const configProblem = (config) => {
171
192
  return 'its home is neither a path (`home: "docs/home.md"`) nor false';
172
193
  if (!["stories", "sources", "docs"].some((key) => isListed(config, key)))
173
194
  return 'it names no stories (`stories: ["src/**/*.stories.tsx"]`) and no sources';
195
+ for (const key of ["stories", "docs"]) {
196
+ const problem = globsProblem(config, key);
197
+ if (problem !== undefined)
198
+ return `its ${key} list nothing: ${problem}`;
199
+ }
174
200
  return framesProblem("frames" in config ? config.frames : undefined);
175
201
  };
176
202
  /**
@@ -213,20 +239,44 @@ const deviceProblem = (device) => {
213
239
  };
214
240
  /**
215
241
  * The configuration's and the presets' lists, once checked where the shell would leave a mistake out: a device that
216
- * cannot be used.
242
+ * cannot be used, a place for the required props that is none, an accessibility setting that is neither rules nor
243
+ * `false`.
217
244
  *
218
245
  * @param all - the presets' lists, then the configuration's.
219
246
  * @param dir - the configuration's folder, for the message.
220
- * @throws when a device cannot be used.
247
+ * @throws when a device cannot be used, `requiredFirst` is none of its layouts, or `a11y` is neither rules nor
248
+ * `false`.
221
249
  */
222
250
  const checkedLists = (all, dir) => {
223
251
  for (const lists of all) {
224
252
  const problem = lists.device === undefined ? undefined : deviceProblem(lists.device);
225
253
  if (problem !== undefined)
226
254
  throw new Error(`${resolve(dir, "forgebook.config")}: Forgebook's device: ${problem}`);
255
+ // a configuration is written by JavaScript too, which no type stops
256
+ if (lists.requiredFirst !== undefined && !REQUIRED_FIRST_LAYOUTS.includes(lists.requiredFirst))
257
+ throw new Error(`${resolve(dir, "forgebook.config")}: requiredFirst is ${typeof lists.requiredFirst === "string" ? `"${lists.requiredFirst}"` : String(lists.requiredFirst)}, none of "in-groups", "section" and false`);
258
+ const a11y = lists.a11y;
259
+ if (a11y !== undefined &&
260
+ a11y !== false &&
261
+ (typeof a11y !== "object" || a11y === null || Array.isArray(a11y)))
262
+ throw new Error(`${resolve(dir, "forgebook.config")}: a11y is ${String(a11y)}: write its rules ({ tags, disabledRules }), or false to check nothing`);
227
263
  }
228
264
  return all;
229
265
  };
266
+ /**
267
+ * Why the merged lists ask for what cannot be, or none: an audience that is none of the ids (the page reads it as one,
268
+ * and a configuration is written by JavaScript too, which no type stops), or "Used in" turned on with the component
269
+ * docs off, whose TypeScript programs read the import graph it is joined from.
270
+ *
271
+ * @param lists - the presets' lists and the configuration's, merged.
272
+ */
273
+ const mergedListsProblem = (lists) => {
274
+ if (lists.audience !== undefined && !AUDIENCE_IDS.includes(lists.audience))
275
+ return `the audience "${lists.audience}" is none of ${AUDIENCE_IDS.join(", ")}`;
276
+ if (lists.usedIn === true && lists.componentDocs === false)
277
+ return "usedIn is true with componentDocs false: Used in reads the import graph with the programs componentDocs turns off";
278
+ return undefined;
279
+ };
230
280
  /** What a catalogue shows: its own stories and docs pages, and the sources; none where it names none. */
231
281
  const shownFiles = (config) => ({
232
282
  stories: config.stories ?? [],
@@ -301,10 +351,9 @@ export const resolveConfig = (config, fileDir, paths = VITE_PATHS) => {
301
351
  const tokenThemed = tokenThemeOf(fromTokens, tokens, dir);
302
352
  const theme = mergedThemes([tokenThemed.theme, ...declaredThemes]);
303
353
  const lists = mergedLists(checkedLists([...presets, config], dir));
304
- // the page reads it as one of the ids, and a configuration is written by JavaScript too, which no type stops
305
- if (lists.audience !== undefined && !AUDIENCE_IDS.includes(lists.audience)) {
306
- throw new Error(`${resolve(dir, "forgebook.config")}: the audience "${lists.audience}" is none of ${AUDIENCE_IDS.join(", ")}`);
307
- }
354
+ const listsProblem = mergedListsProblem(lists);
355
+ if (listsProblem !== undefined)
356
+ throw new Error(`${resolve(dir, "forgebook.config")}: ${listsProblem}`);
308
357
  const themeProblem = themeValueProblem(theme);
309
358
  if (themeProblem !== undefined)
310
359
  throw new Error(`${resolve(dir, "forgebook.config")}: ${themeProblem}`);
@@ -1,6 +1,7 @@
1
1
  import type { DocPageIndex } from "@forgebook/story-format/source";
2
2
  import type { ForgebookPlugin } from "../plugin.ts";
3
- /** A page a pattern matches, with its key and its index. */
3
+ import { type IsFileKept } from "./storyFilesModule.ts";
4
+ /** A page a group lists, with its key and its index. */
4
5
  export type IndexedDocPage = {
5
6
  /** Its path relative to the declaring module (`./docs/theming.md`). */
6
7
  readonly key: string;
@@ -10,24 +11,26 @@ export type IndexedDocPage = {
10
11
  readonly index: DocPageIndex;
11
12
  };
12
13
  /**
13
- * Every page a pattern matches, indexed.
14
+ * Every page a group's patterns list, indexed.
14
15
  *
15
16
  * @param root - the declaring module's folder.
16
- * @param pattern - the pattern.
17
+ * @param patterns - the patterns, `!` ones included (`listedFiles`).
17
18
  * @param watch - adds a file whose edit renews the module that read it.
18
19
  * @param fences - the fences the catalogue's plugins draw, which are not code; none by default.
20
+ * @param isKept - whether a listed page may be read; every one by default.
19
21
  */
20
- export declare const indexedDocPages: (root: string, pattern: string, watch: (file: string) => void, fences?: readonly string[]) => IndexedDocPage[];
22
+ export declare const indexedDocPages: (root: string, patterns: readonly string[], watch: (file: string) => void, fences?: readonly string[], isKept?: IsFileKept) => IndexedDocPage[];
21
23
  /**
22
- * The code of the module for one pattern.
24
+ * The code of the module for one group.
23
25
  *
24
26
  * @param root - the declaring module's folder.
25
- * @param pattern - the pattern before `?forgebook-docs-pages`.
27
+ * @param patterns - the group's patterns.
26
28
  * @param watch - adds a file whose edit renews the module (each page: its index is in the module).
27
29
  * @param isServing - whether a dev server runs, whose tools read a page's Markdown.
28
30
  * @param plugins - the catalogue's plugins, whose fences a page may draw; none by default.
31
+ * @param isKept - whether a listed page may be read; every one by default.
29
32
  */
30
- export declare const docPagesModule: (root: string, pattern: string, watch: (file: string) => void, isServing: boolean, plugins?: readonly ForgebookPlugin[]) => string;
33
+ export declare const docPagesModule: (root: string, patterns: readonly string[], watch: (file: string) => void, isServing: boolean, plugins?: readonly ForgebookPlugin[], isKept?: IsFileKept) => string;
31
34
  /**
32
35
  * The code of one page's module: what the shell draws, each image imported so Vite answers its address, and, on the dev
33
36
  * server, the page's Markdown for tools.