forgebook 0.2.0 → 0.3.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 +113 -5
- package/dist/bin/forgebook/agentLoopCommands.mjs +3 -0
- package/dist/bin/forgebook/agentText.d.ts +2 -2
- package/dist/bin/forgebook/agentText.js +15 -13
- package/dist/bin/forgebook/agents.md +21 -6
- package/dist/bin/forgebook/mcpTools.js +1 -1
- package/dist/bin/forgebook/newStoriesFile.js +7 -2
- package/dist/bin/forgebook-docs/collectDocs.js +1 -1
- package/dist/src/argsProps.d.ts +32 -0
- package/dist/src/argsProps.js +69 -0
- package/dist/src/catalogue.d.ts +48 -5
- package/dist/src/catalogue.js +47 -8
- package/dist/src/frame/mountFrame.js +26 -7
- package/dist/src/frame/touchCaret.d.ts +43 -0
- package/dist/src/frame/touchCaret.js +195 -0
- package/dist/src/frame/touchEvents.d.ts +3 -2
- package/dist/src/frame/touchEvents.js +17 -18
- package/dist/src/frame/touchLayer.js +48 -7
- package/dist/src/propGroups.d.ts +66 -3
- package/dist/src/propGroups.js +67 -13
- package/dist/src/shell/ApiReference.d.ts +9 -4
- package/dist/src/shell/ApiReference.js +47 -39
- package/dist/src/shell/AudienceSections.js +1 -1
- package/dist/src/shell/ComponentPage.d.ts +5 -2
- package/dist/src/shell/ComponentPage.js +4 -18
- package/dist/src/shell/DocBlocks.js +1 -1
- package/dist/src/shell/PropRows.d.ts +42 -0
- package/dist/src/shell/PropRows.js +60 -0
- package/dist/src/shell/PropsTab.d.ts +14 -10
- package/dist/src/shell/PropsTab.js +18 -58
- package/dist/src/shell/RequiredFirstSwitch.d.ts +16 -0
- package/dist/src/shell/RequiredFirstSwitch.js +8 -0
- package/dist/src/shell/Stage.js +11 -23
- package/dist/src/shell/StoryCoverage.js +4 -1
- package/dist/src/shell/docPage.css +6 -0
- package/dist/src/shell/dockTabs.d.ts +7 -5
- package/dist/src/shell/dockTabs.js +4 -4
- package/dist/src/shell/reportedPanel.d.ts +12 -0
- package/dist/src/shell/reportedPanel.js +19 -0
- package/dist/src/shell/shell.css +56 -1
- package/dist/src/shell/strings.d.ts +2 -0
- package/dist/src/shell/strings.js +2 -0
- package/dist/src/shell/usePanelDocs.d.ts +11 -0
- package/dist/src/shell/usePanelDocs.js +29 -0
- package/dist/src/shell/useRequiredFirst.d.ts +27 -0
- package/dist/src/shell/useRequiredFirst.js +67 -0
- package/dist/src/storyCheck.d.ts +2 -0
- package/dist/src/storyCheck.js +2 -1
- package/dist/src/storyGlobals.js +2 -1
- package/dist/src/storyTree.d.ts +11 -1
- package/dist/src/storyTree.js +3 -1
- package/dist/src/viewState.d.ts +7 -0
- package/dist/src/viewState.js +13 -0
- package/dist/src/vite/agent.types.d.ts +12 -0
- package/dist/src/vite/agentApi.js +6 -1
- package/dist/src/vite/agentRoutes.js +18 -4
- package/dist/src/vite/builtTexts.d.ts +2 -2
- package/dist/src/vite/builtTexts.js +7 -6
- package/dist/src/vite/configFile.d.ts +3 -2
- package/dist/src/vite/configFile.js +63 -14
- package/dist/src/vite/docPagesModule.d.ts +10 -7
- package/dist/src/vite/docPagesModule.js +14 -11
- package/dist/src/vite/docsExtractor.d.ts +6 -0
- package/dist/src/vite/docsExtractor.js +13 -0
- package/dist/src/vite/fileAccess.d.ts +75 -0
- package/dist/src/vite/fileAccess.js +199 -0
- package/dist/src/vite/forgebookPlugin.d.ts +1 -1
- package/dist/src/vite/forgebookPlugin.js +67 -98
- package/dist/src/vite/framesServer.js +21 -5
- package/dist/src/vite/generatedModules.d.ts +23 -0
- package/dist/src/vite/generatedModules.js +58 -0
- package/dist/src/vite/hotSnippets.js +7 -3
- package/dist/src/vite/indexStoryFile.d.ts +2 -2
- package/dist/src/vite/indexStoryFile.js +2 -2
- package/dist/src/vite/indexedParameters.d.ts +1 -1
- package/dist/src/vite/indexedParameters.js +21 -1
- package/dist/src/vite/originGuard.d.ts +14 -4
- package/dist/src/vite/originGuard.js +15 -6
- package/dist/src/vite/pages.d.ts +3 -2
- package/dist/src/vite/pages.js +23 -16
- package/dist/src/vite/pluginIds.d.ts +24 -2
- package/dist/src/vite/pluginIds.js +44 -6
- package/dist/src/vite/pluginModules.d.ts +39 -10
- package/dist/src/vite/pluginModules.js +67 -22
- package/dist/src/vite/prebundled.js +2 -1
- package/dist/src/vite/searchModule.d.ts +6 -4
- package/dist/src/vite/searchModule.js +8 -7
- package/dist/src/vite/storyFilesModule.d.ts +59 -27
- package/dist/src/vite/storyFilesModule.js +88 -58
- package/dist/src/vite/storyPatterns.d.ts +39 -0
- package/dist/src/vite/storyPatterns.js +100 -0
- package/dist/src/vite/textQueries.d.ts +7 -1
- package/dist/src/vite/textQueries.js +7 -1
- package/package.json +4 -4
|
@@ -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
|
+
};
|
package/dist/src/storyCheck.d.ts
CHANGED
|
@@ -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.
|
package/dist/src/storyCheck.js
CHANGED
|
@@ -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
|
*
|
package/dist/src/storyGlobals.js
CHANGED
|
@@ -76,7 +76,8 @@ export const fixedViewOf = (globals, parameters, catalogue) => {
|
|
|
76
76
|
*/
|
|
77
77
|
export const withFixed = (view, fixed) => ({
|
|
78
78
|
...view,
|
|
79
|
-
|
|
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
|
});
|
package/dist/src/storyTree.d.ts
CHANGED
|
@@ -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 = {
|
package/dist/src/storyTree.js
CHANGED
|
@@ -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);
|
package/dist/src/viewState.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/src/viewState.js
CHANGED
|
@@ -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) &&
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
103
|
-
return storyFilesGroupText(VITE_PATHS.normalize(root),
|
|
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,
|
|
122
|
-
return textsModuleCode(await read(indexedStoryFiles(root,
|
|
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
|
|
113
|
-
* prop groups' order the
|
|
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
|
|
107
|
-
* prop groups' order the
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
305
|
-
if (
|
|
306
|
-
throw new Error(`${resolve(dir, "forgebook.config")}:
|
|
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
|
-
|
|
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
|
|
14
|
+
* Every page a group's patterns list, indexed.
|
|
14
15
|
*
|
|
15
16
|
* @param root - the declaring module's folder.
|
|
16
|
-
* @param
|
|
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,
|
|
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
|
|
24
|
+
* The code of the module for one group.
|
|
23
25
|
*
|
|
24
26
|
* @param root - the declaring module's folder.
|
|
25
|
-
* @param
|
|
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,
|
|
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.
|