open-ds 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/assets/schemes/accessible-first.json +3093 -0
- package/assets/schemes/brand-expressive.json +3124 -0
- package/assets/schemes/content-first.json +3110 -0
- package/assets/schemes/dense-data.json +3489 -0
- package/assets/schemes/grammar.json +3903 -0
- package/assets/schemes/multi-brand.json +4359 -0
- package/assets/skills/open-ds/SKILL.md +50 -0
- package/assets/skills/open-ds-build/SKILL.md +19 -0
- package/assets/skills/open-ds-change/SKILL.md +19 -0
- package/assets/skills/open-ds-choose/SKILL.md +18 -0
- package/assets/skills/open-ds-review/SKILL.md +22 -0
- package/assets/skills/open-ds-write/SKILL.md +13 -0
- package/assets/stories/accordion.stories.tsx +46 -0
- package/assets/stories/alert-dialog.stories.tsx +65 -0
- package/assets/stories/alert.stories.tsx +44 -0
- package/assets/stories/aspect-ratio.stories.tsx +37 -0
- package/assets/stories/avatar.stories.tsx +42 -0
- package/assets/stories/badge.stories.tsx +39 -0
- package/assets/stories/breadcrumb.stories.tsx +54 -0
- package/assets/stories/button.stories.tsx +46 -0
- package/assets/stories/calendar.stories.tsx +30 -0
- package/assets/stories/card.stories.tsx +50 -0
- package/assets/stories/carousel.stories.tsx +64 -0
- package/assets/stories/chart.stories.tsx +61 -0
- package/assets/stories/checkbox.stories.tsx +29 -0
- package/assets/stories/collapsible.stories.tsx +39 -0
- package/assets/stories/combobox.stories.tsx +57 -0
- package/assets/stories/command.stories.tsx +60 -0
- package/assets/stories/context-menu.stories.tsx +55 -0
- package/assets/stories/data-table.stories.tsx +93 -0
- package/assets/stories/date-picker.stories.tsx +27 -0
- package/assets/stories/dialog.stories.tsx +70 -0
- package/assets/stories/drawer.stories.tsx +62 -0
- package/assets/stories/dropdown-menu.stories.tsx +65 -0
- package/assets/stories/empty.stories.tsx +39 -0
- package/assets/stories/field.stories.tsx +42 -0
- package/assets/stories/form.stories.tsx +56 -0
- package/assets/stories/input-group.stories.tsx +41 -0
- package/assets/stories/input-otp.stories.tsx +48 -0
- package/assets/stories/input.stories.tsx +32 -0
- package/assets/stories/kbd.stories.tsx +30 -0
- package/assets/stories/label.stories.tsx +26 -0
- package/assets/stories/native-select.stories.tsx +39 -0
- package/assets/stories/navigation-menu.stories.tsx +41 -0
- package/assets/stories/pagination.stories.tsx +61 -0
- package/assets/stories/popover.stories.tsx +37 -0
- package/assets/stories/progress.stories.tsx +30 -0
- package/assets/stories/radio-group.stories.tsx +45 -0
- package/assets/stories/resizable.stories.tsx +54 -0
- package/assets/stories/scroll-area.stories.tsx +47 -0
- package/assets/stories/select.stories.tsx +57 -0
- package/assets/stories/separator.stories.tsx +28 -0
- package/assets/stories/sheet.stories.tsx +46 -0
- package/assets/stories/sidebar.stories.tsx +62 -0
- package/assets/stories/skeleton.stories.tsx +27 -0
- package/assets/stories/slider.stories.tsx +44 -0
- package/assets/stories/spinner.stories.tsx +25 -0
- package/assets/stories/switch.stories.tsx +29 -0
- package/assets/stories/table.stories.tsx +97 -0
- package/assets/stories/tabs.stories.tsx +55 -0
- package/assets/stories/textarea.stories.tsx +28 -0
- package/assets/stories/toast.stories.tsx +30 -0
- package/assets/stories/toggle-group.stories.tsx +41 -0
- package/assets/stories/toggle.stories.tsx +20 -0
- package/assets/stories/tooltip.stories.tsx +49 -0
- package/assets/tokens/accessible-first/content.json +32 -0
- package/assets/tokens/accessible-first/dark.tokens.json +581 -0
- package/assets/tokens/accessible-first/light.tokens.json +581 -0
- package/assets/tokens/accessible-first/shadcn.css +21 -0
- package/assets/tokens/accessible-first/theme.css +101 -0
- package/assets/tokens/accessible-first/tokens.css +331 -0
- package/assets/tokens/accessible-first/tokens.json +1616 -0
- package/assets/tokens/accessible-first/tokens.scoped.css +333 -0
- package/assets/tokens/brand-expressive/content.json +32 -0
- package/assets/tokens/brand-expressive/dark.tokens.json +585 -0
- package/assets/tokens/brand-expressive/light.tokens.json +585 -0
- package/assets/tokens/brand-expressive/shadcn.css +21 -0
- package/assets/tokens/brand-expressive/theme.css +101 -0
- package/assets/tokens/brand-expressive/tokens.css +331 -0
- package/assets/tokens/brand-expressive/tokens.json +1651 -0
- package/assets/tokens/brand-expressive/tokens.scoped.css +333 -0
- package/assets/tokens/content-first/content.json +32 -0
- package/assets/tokens/content-first/dark.tokens.json +582 -0
- package/assets/tokens/content-first/light.tokens.json +582 -0
- package/assets/tokens/content-first/shadcn.css +21 -0
- package/assets/tokens/content-first/theme.css +101 -0
- package/assets/tokens/content-first/tokens.css +331 -0
- package/assets/tokens/content-first/tokens.json +1631 -0
- package/assets/tokens/content-first/tokens.scoped.css +333 -0
- package/assets/tokens/dense-data/content.json +32 -0
- package/assets/tokens/dense-data/dark.tokens.json +580 -0
- package/assets/tokens/dense-data/light.tokens.json +580 -0
- package/assets/tokens/dense-data/shadcn.css +21 -0
- package/assets/tokens/dense-data/theme.css +101 -0
- package/assets/tokens/dense-data/tokens.css +331 -0
- package/assets/tokens/dense-data/tokens.json +1623 -0
- package/assets/tokens/dense-data/tokens.scoped.css +333 -0
- package/assets/tokens/index.json +143 -0
- package/assets/tokens/multi-brand/aster-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/aster-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/birch-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/birch-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/content.json +32 -0
- package/assets/tokens/multi-brand/coral-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/coral-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/shadcn.css +21 -0
- package/assets/tokens/multi-brand/theme.css +101 -0
- package/assets/tokens/multi-brand/tokens.css +1002 -0
- package/assets/tokens/multi-brand/tokens.json +1754 -0
- package/assets/tokens/multi-brand/tokens.scoped.css +1022 -0
- package/assets/tokens/shadcn-map.json +34 -0
- package/assets/ui/components/accordion.tsx +60 -0
- package/assets/ui/components/alert-dialog.tsx +171 -0
- package/assets/ui/components/alert.tsx +56 -0
- package/assets/ui/components/aspect-ratio.tsx +17 -0
- package/assets/ui/components/avatar.tsx +96 -0
- package/assets/ui/components/badge.tsx +36 -0
- package/assets/ui/components/breadcrumb.tsx +99 -0
- package/assets/ui/components/button.tsx +53 -0
- package/assets/ui/components/calendar.tsx +169 -0
- package/assets/ui/components/card.tsx +72 -0
- package/assets/ui/components/carousel.tsx +224 -0
- package/assets/ui/components/chart.tsx +315 -0
- package/assets/ui/components/checkbox.tsx +29 -0
- package/assets/ui/components/collapsible.tsx +25 -0
- package/assets/ui/components/combobox.tsx +275 -0
- package/assets/ui/components/command.tsx +145 -0
- package/assets/ui/components/context-menu.tsx +218 -0
- package/assets/ui/components/data-table.tsx +307 -0
- package/assets/ui/components/date-picker.tsx +79 -0
- package/assets/ui/components/dialog.tsx +68 -0
- package/assets/ui/components/drawer.tsx +115 -0
- package/assets/ui/components/dropdown-menu.tsx +58 -0
- package/assets/ui/components/empty.tsx +92 -0
- package/assets/ui/components/field.tsx +234 -0
- package/assets/ui/components/form.tsx +146 -0
- package/assets/ui/components/input-group.tsx +154 -0
- package/assets/ui/components/input-otp.tsx +75 -0
- package/assets/ui/components/input.tsx +25 -0
- package/assets/ui/components/kbd.tsx +30 -0
- package/assets/ui/components/label.tsx +22 -0
- package/assets/ui/components/native-select.tsx +59 -0
- package/assets/ui/components/navigation-menu.tsx +149 -0
- package/assets/ui/components/pagination.tsx +108 -0
- package/assets/ui/components/popover.tsx +61 -0
- package/assets/ui/components/progress.tsx +31 -0
- package/assets/ui/components/radio-group.tsx +40 -0
- package/assets/ui/components/resizable.tsx +54 -0
- package/assets/ui/components/scroll-area.tsx +55 -0
- package/assets/ui/components/select.tsx +81 -0
- package/assets/ui/components/separator.tsx +35 -0
- package/assets/ui/components/sheet.tsx +114 -0
- package/assets/ui/components/sidebar.tsx +685 -0
- package/assets/ui/components/skeleton.tsx +15 -0
- package/assets/ui/components/slider.tsx +70 -0
- package/assets/ui/components/spinner.tsx +16 -0
- package/assets/ui/components/switch.tsx +32 -0
- package/assets/ui/components/table.tsx +78 -0
- package/assets/ui/components/tabs.tsx +49 -0
- package/assets/ui/components/textarea.tsx +25 -0
- package/assets/ui/components/toast.tsx +54 -0
- package/assets/ui/components/toggle-group.tsx +88 -0
- package/assets/ui/components/toggle.tsx +48 -0
- package/assets/ui/components/tooltip.tsx +34 -0
- package/assets/ui/lib/use-mobile.ts +20 -0
- package/assets/ui/lib/utils.ts +7 -0
- package/assets/ui/styles.css +27 -0
- package/bin/open-ds.js +6 -0
- package/package.json +56 -4
- package/src/agents.mjs +150 -0
- package/src/cli.mjs +445 -0
- package/src/components.mjs +69 -0
- package/src/context.mjs +143 -0
- package/src/create.mjs +377 -0
- package/src/docs.mjs +570 -0
- package/src/guidance.mjs +63 -0
- package/src/index.mjs +20 -0
- package/src/mcp.mjs +660 -0
- package/src/readmes.mjs +106 -0
- package/src/schemes.mjs +188 -0
- package/src/tokens.mjs +127 -0
- package/templates/project/.github/workflows/ci.yml +15 -0
- package/templates/project/.storybook/component-docs.mdx +26 -0
- package/templates/project/.storybook/main.ts +17 -0
- package/templates/project/.storybook/manager-head.html +3 -0
- package/templates/project/.storybook/manager.ts +37 -0
- package/templates/project/.storybook/preview-head.html +3 -0
- package/templates/project/.storybook/preview.tsx +82 -0
- package/templates/project/docs/docusaurus.config.ts +64 -0
- package/templates/project/docs/sidebars.ts +8 -0
- package/templates/project/docs/src/css/custom.css +275 -0
- package/templates/project/gitignore +6 -0
- package/templates/project/src/styles/index.css +16 -0
- package/templates/project/src/vite-env.d.ts +7 -0
- package/templates/project/stories/docs-blocks.tsx +190 -0
- package/templates/project/stories/samples.tsx +102 -0
- package/templates/project/stories/usage.tsx +31 -0
- package/templates/project/tokens/build.mjs +468 -0
- package/templates/project/tsconfig.json +15 -0
package/src/docs.mjs
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
// Writes the documentation of a delivered design system: one scheme, as Markdown pages for
|
|
2
|
+
// Docusaurus. The docs site (docs/) and `open-ds init --docs` both use this, so a team that takes
|
|
3
|
+
// a scheme gets the same pages Open DS builds.
|
|
4
|
+
|
|
5
|
+
import { cssName, semanticView, themeTokens } from "./tokens.mjs";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { questions } from "./schemes.mjs";
|
|
9
|
+
|
|
10
|
+
// Which component keys each shipped component reads.
|
|
11
|
+
const KEY_GROUP = {
|
|
12
|
+
button: "button",
|
|
13
|
+
input: "input",
|
|
14
|
+
select: "input",
|
|
15
|
+
label: "input",
|
|
16
|
+
checkbox: "control",
|
|
17
|
+
switch: "control",
|
|
18
|
+
badge: "badge",
|
|
19
|
+
card: "card",
|
|
20
|
+
table: "table",
|
|
21
|
+
tabs: "tabs",
|
|
22
|
+
dialog: "overlay",
|
|
23
|
+
"dropdown-menu": "overlay",
|
|
24
|
+
toast: "overlay",
|
|
25
|
+
tooltip: "overlay",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fm = (o) =>
|
|
29
|
+
`---\n${Object.entries(o)
|
|
30
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
31
|
+
.join("\n")}\n---\n\n`;
|
|
32
|
+
const NOTE =
|
|
33
|
+
"<!-- Generated by open-ds from the scheme. Do not edit by hand. -->\n\n";
|
|
34
|
+
const esc = (s) =>
|
|
35
|
+
String(s)
|
|
36
|
+
.replaceAll("|", "\\|")
|
|
37
|
+
.replaceAll("\n", " ")
|
|
38
|
+
.replaceAll("<", "<")
|
|
39
|
+
.replaceAll("{", "{")
|
|
40
|
+
.replaceAll("}", "}");
|
|
41
|
+
const title = (id) =>
|
|
42
|
+
id.charAt(0).toUpperCase() + id.slice(1).replaceAll("-", " ");
|
|
43
|
+
const get = (tree, path) =>
|
|
44
|
+
path
|
|
45
|
+
.split(".")
|
|
46
|
+
.reduce((n, p) => (n && typeof n === "object" ? n[p] : undefined), tree);
|
|
47
|
+
const isToken = (v) => v && typeof v === "object" && "$value" in v;
|
|
48
|
+
|
|
49
|
+
function merge(a, b) {
|
|
50
|
+
const out = { ...a };
|
|
51
|
+
for (const [k, v] of Object.entries(b ?? {}))
|
|
52
|
+
out[k] =
|
|
53
|
+
v && typeof v === "object" && !isToken(v) && out[k]
|
|
54
|
+
? merge(out[k], v)
|
|
55
|
+
: v;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function leaves(node, path) {
|
|
60
|
+
if (isToken(node))
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
path,
|
|
64
|
+
ref: node.$value,
|
|
65
|
+
type: node.$type,
|
|
66
|
+
description: node.$description,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
if (!node || typeof node !== "object") return [];
|
|
70
|
+
return Object.entries(node)
|
|
71
|
+
.filter(([k]) => !k.startsWith("$"))
|
|
72
|
+
.flatMap(([k, v]) => leaves(v, `${path}.${k}`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The tokens a scope owns in this scheme: semantics, brand primitives, component keys or words. */
|
|
76
|
+
function owned(scheme, owns) {
|
|
77
|
+
const t = scheme.tokens;
|
|
78
|
+
const theme = Object.values(t.brands)[0].theme;
|
|
79
|
+
const sources = [
|
|
80
|
+
semanticView(scheme, "light"),
|
|
81
|
+
{ theme: merge(theme.base ?? {}, theme.light) },
|
|
82
|
+
{ component: t.component },
|
|
83
|
+
{ content: t.content },
|
|
84
|
+
];
|
|
85
|
+
return owns.flatMap((o) => {
|
|
86
|
+
const tree = sources.find((x) => get(x, o) !== undefined);
|
|
87
|
+
return tree ? leaves(get(tree, o), o) : [];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const show = (v) =>
|
|
92
|
+
v === undefined
|
|
93
|
+
? ""
|
|
94
|
+
: typeof v === "string"
|
|
95
|
+
? v
|
|
96
|
+
: Array.isArray(v)
|
|
97
|
+
? `cubic-bezier(${v.join(", ")})`
|
|
98
|
+
: typeof v === "object"
|
|
99
|
+
? Object.values(v).join(" ")
|
|
100
|
+
: String(v);
|
|
101
|
+
const px = (v) =>
|
|
102
|
+
/^-?[\d.]+rem$/.test(v) ? `${v} (${Math.round(parseFloat(v) * 16)}px)` : v;
|
|
103
|
+
|
|
104
|
+
const cssVar = (path) => `var(--${cssName(path)})`;
|
|
105
|
+
const attr = (s) => String(s).replaceAll('"', """);
|
|
106
|
+
|
|
107
|
+
/** One theme's tokens before resolving: primitives, the brand, the semantics for the mode, component keys. */
|
|
108
|
+
function rawTree(scheme, theme) {
|
|
109
|
+
return themeTokens(scheme, theme.brand, theme.mode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The aliases a token points through, ending at a primitive. Raw is set when the chain ends in a value. */
|
|
113
|
+
function chain(tree, path) {
|
|
114
|
+
const steps = [];
|
|
115
|
+
let at = path;
|
|
116
|
+
for (let i = 0; i < 10; i++) {
|
|
117
|
+
const node = get(tree, at);
|
|
118
|
+
if (!isToken(node)) break;
|
|
119
|
+
const m =
|
|
120
|
+
typeof node.$value === "string" && node.$value.match(/^\{(.+)\}$/);
|
|
121
|
+
if (!m) return { steps, raw: node.$value };
|
|
122
|
+
steps.push(m[1]);
|
|
123
|
+
at = m[1];
|
|
124
|
+
}
|
|
125
|
+
return { steps, raw: undefined };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A small live sample of a primitive or semantic value, drawn from its CSS variable. */
|
|
129
|
+
function sample(path, type) {
|
|
130
|
+
const v = cssVar(path);
|
|
131
|
+
if (type === "color")
|
|
132
|
+
return `<span class="ods-swatch" style="background:${v}"></span>`;
|
|
133
|
+
if (type === "fontFamily")
|
|
134
|
+
return `<span class="ods-font" style="font-family:${v}">Aa 123</span>`;
|
|
135
|
+
if (type === "shadow")
|
|
136
|
+
return `<span class="ods-card" style="box-shadow:${v}"></span>`;
|
|
137
|
+
if (type === "duration")
|
|
138
|
+
return `<span class="ods-motion" style="--ods-d:${v}"><span></span></span>`;
|
|
139
|
+
if (type === "number" && /opacity/.test(path))
|
|
140
|
+
return `<span class="ods-swatch" style="opacity:${v}"></span>`;
|
|
141
|
+
if (type === "dimension") {
|
|
142
|
+
if (/radius/.test(path))
|
|
143
|
+
return `<span class="ods-tile" style="border-radius:${v}"></span>`;
|
|
144
|
+
if (/border-width/.test(path))
|
|
145
|
+
return `<span class="ods-line" style="height:${v}"></span>`;
|
|
146
|
+
if (/font-size|line-height|letter/.test(path)) return "";
|
|
147
|
+
return `<span class="ods-bar" style="width:${v}"></span>`;
|
|
148
|
+
}
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The specimen at the top of a foundation page: the scope's semantic tokens, drawn live. */
|
|
153
|
+
function specimen(tokens, text) {
|
|
154
|
+
const roles = [
|
|
155
|
+
...new Set(
|
|
156
|
+
tokens
|
|
157
|
+
.map((t) => t.path.match(/^type\.([a-z-]+)\.font-size$/)?.[1])
|
|
158
|
+
.filter(Boolean),
|
|
159
|
+
),
|
|
160
|
+
];
|
|
161
|
+
if (roles.length) {
|
|
162
|
+
return (
|
|
163
|
+
`<div class="ods-specimen">\n` +
|
|
164
|
+
roles
|
|
165
|
+
.map((r) => {
|
|
166
|
+
const p = `type.${r}`;
|
|
167
|
+
const style = `font-family:${cssVar(`${p}.font-family`)};font-size:${cssVar(`${p}.font-size`)};line-height:${cssVar(`${p}.line-height`)};font-weight:${cssVar(`${p}.font-weight`)}`;
|
|
168
|
+
return `<div class="ods-type"><span class="ods-label">${r}</span><span style="${style}">${attr(text)}</span></div>`;
|
|
169
|
+
})
|
|
170
|
+
.join("\n") +
|
|
171
|
+
`\n</div>\n\n`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const items = tokens
|
|
175
|
+
.map((t) => ({ t, s: sample(t.path, t.type) }))
|
|
176
|
+
.filter((x) => x.s)
|
|
177
|
+
.slice(0, 40);
|
|
178
|
+
if (!items.length) return "";
|
|
179
|
+
return (
|
|
180
|
+
`<div class="ods-specimen">\n` +
|
|
181
|
+
items
|
|
182
|
+
.map(
|
|
183
|
+
({ t, s }) =>
|
|
184
|
+
`<span class="ods-item">${s}<span class="ods-label">${t.path}</span></span>`,
|
|
185
|
+
)
|
|
186
|
+
.join("\n") +
|
|
187
|
+
`\n</div>\n\n`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The header comment of a component file, as plain text. */
|
|
192
|
+
function componentIntro(uiDir, name) {
|
|
193
|
+
const path = join(uiDir, "components", `${name}.tsx`);
|
|
194
|
+
if (!existsSync(path)) return "";
|
|
195
|
+
const m = readFileSync(path, "utf8").match(/^\/\*\*([\s\S]*?)\*\//);
|
|
196
|
+
return m
|
|
197
|
+
? m[1]
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((l) => l.replace(/^\s*\* ?/, ""))
|
|
200
|
+
.join(" ")
|
|
201
|
+
.replace(/\s+/g, " ")
|
|
202
|
+
.trim()
|
|
203
|
+
: "";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Every page of one scheme's docs, as { "relative/path.md": text }.
|
|
208
|
+
* roots: { tokens, ui } from assetRoots(). storybook: the Storybook URL to link to.
|
|
209
|
+
*/
|
|
210
|
+
/** The colour scope's guidance, from the grammar: the urgency order, what each key is for, and its pairs. */
|
|
211
|
+
function colourGuidance(g) {
|
|
212
|
+
const s = g.semantics;
|
|
213
|
+
if (!s) return "";
|
|
214
|
+
const keys = s.keys;
|
|
215
|
+
return (
|
|
216
|
+
`\n## Status urgency\n\n${s.urgency.description}\n\n${s.urgency.order.map((id) => `\`${id}\``).join(" → ")}\n` +
|
|
217
|
+
`\n## What each colour is for\n\n| Token | What it is for | Not for |\n| --- | --- | --- |\n` +
|
|
218
|
+
keys
|
|
219
|
+
.map(
|
|
220
|
+
(k) =>
|
|
221
|
+
`| \`${k.id}\` | ${esc(k.intent)} | ${k.not_for.map((n) => `${esc(n.when)}: \`${n.use}\``).join("<br />") || "—"} |`,
|
|
222
|
+
)
|
|
223
|
+
.join("\n") +
|
|
224
|
+
`\n\n## Pairs at a glance\n\nWhich colour may sit on which fill, and the contrast it keeps there in every brand and mode. The harness checks every pair.\n\n| Token | Sits on | Contrast |\n| --- | --- | --- |\n` +
|
|
225
|
+
keys
|
|
226
|
+
.filter((k) => k.pairs)
|
|
227
|
+
.map(
|
|
228
|
+
(k) =>
|
|
229
|
+
`| \`${k.id}\` | ${k.pairs.on.map((o) => `\`${o}\``).join(", ")} | ${k.pairs.contrast ? `${k.pairs.contrast}:1` : esc(k.exempt ?? "exempt")} |`,
|
|
230
|
+
)
|
|
231
|
+
.join("\n") +
|
|
232
|
+
"\n"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function docsPages(
|
|
237
|
+
scheme,
|
|
238
|
+
g,
|
|
239
|
+
roots,
|
|
240
|
+
{ storybook = "http://localhost:6006" } = {},
|
|
241
|
+
) {
|
|
242
|
+
const qs = questions(g);
|
|
243
|
+
const sits = Object.fromEntries(g.situations.map((s) => [s.id, s]));
|
|
244
|
+
const answer = (id) => scheme.answers.find((a) => a.question === id);
|
|
245
|
+
const label = (id) =>
|
|
246
|
+
qs[id].options.find((o) => o.id === answer(id).option).label;
|
|
247
|
+
const because = (ids) =>
|
|
248
|
+
ids.map((b) => `${qs[b].text} ${label(b)}`).join("; ");
|
|
249
|
+
const themes = Object.fromEntries(
|
|
250
|
+
scheme.themes.map((t) => [
|
|
251
|
+
t.id,
|
|
252
|
+
JSON.parse(
|
|
253
|
+
readFileSync(
|
|
254
|
+
join(roots.tokens, scheme.id, `${t.id}.tokens.json`),
|
|
255
|
+
"utf8",
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
]),
|
|
259
|
+
);
|
|
260
|
+
const first = scheme.themes[0];
|
|
261
|
+
const dark = scheme.themes.find(
|
|
262
|
+
(t) => t.brand === first.brand && t.mode === "dark",
|
|
263
|
+
);
|
|
264
|
+
const value = (theme, path, ref) => {
|
|
265
|
+
const v = get(themes[theme.id], path);
|
|
266
|
+
return px(show(v === undefined ? ref : v));
|
|
267
|
+
};
|
|
268
|
+
const trees = Object.fromEntries(
|
|
269
|
+
scheme.themes.map((t) => [t.id, rawTree(scheme, t)]),
|
|
270
|
+
);
|
|
271
|
+
const primitiveTree = scheme.tokens.primitive;
|
|
272
|
+
const isPrimitive = (path) => isToken(get(primitiveTree, path));
|
|
273
|
+
// A semantic or component token shows what it points to, never a raw value. Only primitives have values.
|
|
274
|
+
const link = (theme, path) => {
|
|
275
|
+
const { steps, raw } = chain(trees[theme.id], path);
|
|
276
|
+
if (steps.length) return steps.map((x) => `\`${x}\``).join(" → ");
|
|
277
|
+
return raw === undefined
|
|
278
|
+
? ""
|
|
279
|
+
: `${esc(px(show(raw)))} (set here, not a primitive)`;
|
|
280
|
+
};
|
|
281
|
+
const primitiveOf = (theme, path) => {
|
|
282
|
+
const { steps } = chain(trees[theme.id], path);
|
|
283
|
+
return steps.length && isPrimitive(steps.at(-1)) ? steps.at(-1) : undefined;
|
|
284
|
+
};
|
|
285
|
+
const primitiveRows = (paths) =>
|
|
286
|
+
`| Primitive | Value | Sample |\n| --- | --- | --- |\n` +
|
|
287
|
+
[...new Set(paths)]
|
|
288
|
+
.filter(Boolean)
|
|
289
|
+
.sort()
|
|
290
|
+
.map(
|
|
291
|
+
(p) =>
|
|
292
|
+
`| \`${p}\` | ${esc(value(first, p, ""))} | ${sample(p, get(primitiveTree, p)?.$type)} |`,
|
|
293
|
+
)
|
|
294
|
+
.join("\n");
|
|
295
|
+
const rule = (u) =>
|
|
296
|
+
`| ${esc(sits[u.situation].title)} | ${esc(u.use)} | ${esc(u.not)} | ${esc(u.why)}${u.example ? ` Example: “${esc(u.example)}”` : ""} |`;
|
|
297
|
+
const RULES = "| Situation | Use | Not | Why |\n| --- | --- | --- | --- |\n";
|
|
298
|
+
const pages = {};
|
|
299
|
+
|
|
300
|
+
// About this system
|
|
301
|
+
pages["index.md"] =
|
|
302
|
+
fm({
|
|
303
|
+
sidebar_position: 1,
|
|
304
|
+
slug: "/",
|
|
305
|
+
title: scheme.name,
|
|
306
|
+
description: scheme.summary,
|
|
307
|
+
}) +
|
|
308
|
+
NOTE +
|
|
309
|
+
`# ${scheme.name}\n\n${scheme.summary}\n\n` +
|
|
310
|
+
`- **Use it for:** ${scheme.ai.use_when}\n- **Not for:** ${scheme.ai.avoid_when}\n- **Products like:** ${scheme.ai.example_products.join(", ")}\n- **Themes:** ${scheme.themes.map((t) => t.id).join(", ")}\n\n` +
|
|
311
|
+
`## Use scenarios\n\n${scheme.scenarios.map((s) => `- ${s.text}`).join("\n")}\n\n` +
|
|
312
|
+
`## Why this design system is the way it is\n\nEvery decision in this system follows from these answers.\n\n` +
|
|
313
|
+
g.reasons
|
|
314
|
+
.map(
|
|
315
|
+
(r) =>
|
|
316
|
+
`### ${r.title}\n\n${r.questions
|
|
317
|
+
.map((q) => {
|
|
318
|
+
const a = answer(q.id);
|
|
319
|
+
return `- **${q.text}** ${label(q.id)}. ${a.why}${a.because.length ? ` Because: ${because(a.because)}.` : ""}`;
|
|
320
|
+
})
|
|
321
|
+
.join("\n")}`,
|
|
322
|
+
)
|
|
323
|
+
.join("\n\n") +
|
|
324
|
+
"\n";
|
|
325
|
+
|
|
326
|
+
// Voice and tone
|
|
327
|
+
const v = scheme.voice;
|
|
328
|
+
const voiceRules = scheme.usage.filter(
|
|
329
|
+
(u) => sits[u.situation].scope === "voice",
|
|
330
|
+
);
|
|
331
|
+
pages["voice.md"] =
|
|
332
|
+
fm({
|
|
333
|
+
sidebar_position: 2,
|
|
334
|
+
title: "Voice and tone",
|
|
335
|
+
description: v.summary.text,
|
|
336
|
+
}) +
|
|
337
|
+
NOTE +
|
|
338
|
+
`# Voice and tone\n\n${v.summary.text}\n\n## Principles\n\n${v.principles.map((p) => `- ${p.text}`).join("\n")}\n\n` +
|
|
339
|
+
(v.brands
|
|
340
|
+
? `## Each brand\n\n${Object.entries(v.brands)
|
|
341
|
+
.map(([b, t]) => `- **${title(b)}:** ${t.text}`)
|
|
342
|
+
.join("\n")}\n\n`
|
|
343
|
+
: "") +
|
|
344
|
+
`## Words\n\n- Use: ${v.words.use.join(", ")}\n- Avoid: ${v.words.avoid.join(", ")}\n\n` +
|
|
345
|
+
`## Tone for each moment\n\n${RULES}${voiceRules.map(rule).join("\n")}\n`;
|
|
346
|
+
|
|
347
|
+
// Foundations: one page per scope, the same parts for every scope.
|
|
348
|
+
pages["foundations/_category_.json"] =
|
|
349
|
+
JSON.stringify(
|
|
350
|
+
{
|
|
351
|
+
label: "Foundations",
|
|
352
|
+
position: 3,
|
|
353
|
+
link: { type: "doc", id: "foundations/index" },
|
|
354
|
+
},
|
|
355
|
+
null,
|
|
356
|
+
2,
|
|
357
|
+
) + "\n";
|
|
358
|
+
// The foundations overview: every scope in one compact table, with the answer that sets its values.
|
|
359
|
+
pages["foundations/index.md"] =
|
|
360
|
+
fm({
|
|
361
|
+
title: "Foundations",
|
|
362
|
+
description: `${scheme.name}: every scope and the answer that sets its values.`,
|
|
363
|
+
}) +
|
|
364
|
+
NOTE +
|
|
365
|
+
`# Foundations\n\nEvery scope answers the same five questions. This is the answer that sets each scope's values; open a scope for all five, its tokens and its specimen.\n\n` +
|
|
366
|
+
`| Scope | Values | Why |\n| --- | --- | --- |\n` +
|
|
367
|
+
g.scopes
|
|
368
|
+
.map((sc) => {
|
|
369
|
+
const q =
|
|
370
|
+
sc.questions.find((x) => x.method === "values") ?? sc.questions[3];
|
|
371
|
+
return `| [${sc.title}](./${sc.id}.md) | ${esc(label(q.id))} | ${esc(answer(q.id).why)} |`;
|
|
372
|
+
})
|
|
373
|
+
.join("\n") +
|
|
374
|
+
"\n";
|
|
375
|
+
g.scopes.forEach((sc, i) => {
|
|
376
|
+
const tokens = owned(scheme, sc.owns ?? []);
|
|
377
|
+
const usage = scheme.usage.filter((u) => sits[u.situation].scope === sc.id);
|
|
378
|
+
// Paths for the component scope start with component.; the key itself is the rest.
|
|
379
|
+
const key = (p) => p.replace(/^component\./, "");
|
|
380
|
+
const semantics = tokens.filter((t) => !isPrimitive(t.path));
|
|
381
|
+
const darkDiffers = semantics.some(
|
|
382
|
+
(t) => link(first, key(t.path)) !== link(dark, key(t.path)),
|
|
383
|
+
);
|
|
384
|
+
const prims = [
|
|
385
|
+
...tokens.filter((t) => isPrimitive(t.path)).map((t) => t.path),
|
|
386
|
+
...semantics.flatMap((t) => [
|
|
387
|
+
primitiveOf(first, key(t.path)),
|
|
388
|
+
primitiveOf(dark, key(t.path)),
|
|
389
|
+
]),
|
|
390
|
+
];
|
|
391
|
+
const SPECIMEN = ["components", "naming", "voice"].includes(sc.id)
|
|
392
|
+
? ""
|
|
393
|
+
: specimen(
|
|
394
|
+
semantics.length ? semantics : tokens,
|
|
395
|
+
scheme.scenarios[0].text,
|
|
396
|
+
);
|
|
397
|
+
pages[`foundations/${sc.id}.md`] =
|
|
398
|
+
fm({
|
|
399
|
+
sidebar_position: i + 1,
|
|
400
|
+
title: sc.title,
|
|
401
|
+
description: `${scheme.name}: ${sc.description}`,
|
|
402
|
+
}) +
|
|
403
|
+
NOTE +
|
|
404
|
+
`# ${sc.title}\n\n${sc.description}\n\n${SPECIMEN}## The five answers\n\n| Question | Answer | Why |\n| --- | --- | --- |\n` +
|
|
405
|
+
sc.questions
|
|
406
|
+
.map(
|
|
407
|
+
(q) =>
|
|
408
|
+
`| ${esc(q.text)} | ${esc(label(q.id))} | ${esc(answer(q.id).why)} |`,
|
|
409
|
+
)
|
|
410
|
+
.join("\n") +
|
|
411
|
+
`\n\n## Tokens\n\n` +
|
|
412
|
+
(semantics.length
|
|
413
|
+
? `Each token points to another token. Values live only on primitives, below.\n\n` +
|
|
414
|
+
(darkDiffers
|
|
415
|
+
? "| Token | Points to, light | Points to, dark |\n| --- | --- | --- |\n"
|
|
416
|
+
: "| Token | Points to |\n| --- | --- |\n") +
|
|
417
|
+
semantics
|
|
418
|
+
.map((t) =>
|
|
419
|
+
darkDiffers
|
|
420
|
+
? `| \`${t.path}\` | ${link(first, key(t.path))} | ${link(dark, key(t.path))} |`
|
|
421
|
+
: `| \`${t.path}\` | ${link(first, key(t.path))} |`,
|
|
422
|
+
)
|
|
423
|
+
.join("\n")
|
|
424
|
+
: "This scope sets primitives directly.") +
|
|
425
|
+
(prims.filter(Boolean).length
|
|
426
|
+
? `\n\n## Primitives\n\nThe raw values the tokens above end at.\n\n${primitiveRows(prims)}`
|
|
427
|
+
: "") +
|
|
428
|
+
`\n\n## Where it lands\n\n${(sc.lands_in ?? []).map((c) => `- ${title(c)}`).join("\n")}\n\n` +
|
|
429
|
+
`## When to use what\n\n${RULES}${usage.map(rule).join("\n")}\n` +
|
|
430
|
+
(sc.id === "color" ? colourGuidance(g) : "");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Components
|
|
434
|
+
pages["components/_category_.json"] =
|
|
435
|
+
JSON.stringify(
|
|
436
|
+
{
|
|
437
|
+
label: "Components",
|
|
438
|
+
position: 4,
|
|
439
|
+
link: { type: "doc", id: "components/inventory" },
|
|
440
|
+
},
|
|
441
|
+
null,
|
|
442
|
+
2,
|
|
443
|
+
) + "\n";
|
|
444
|
+
const built = readdirSync(join(roots.ui, "components"))
|
|
445
|
+
.filter((f) => f.endsWith(".tsx"))
|
|
446
|
+
.map((f) => f.replace(/\.tsx$/, ""))
|
|
447
|
+
.sort();
|
|
448
|
+
const inventory = Object.fromEntries(
|
|
449
|
+
(scheme.inventory?.in ?? []).map((i) => [i.id, i.why]),
|
|
450
|
+
);
|
|
451
|
+
const out = Object.fromEntries(
|
|
452
|
+
(scheme.inventory?.out ?? []).map((i) => [i.id, i.why]),
|
|
453
|
+
);
|
|
454
|
+
const catalogue = g.catalogue?.items ?? [];
|
|
455
|
+
// The inventory page: every catalogue item, in or out, and whether it is built yet.
|
|
456
|
+
pages["components/inventory.md"] =
|
|
457
|
+
fm({
|
|
458
|
+
sidebar_position: 0,
|
|
459
|
+
title: "Inventory",
|
|
460
|
+
description: `The components ${scheme.name} carries.`,
|
|
461
|
+
}) +
|
|
462
|
+
NOTE +
|
|
463
|
+
`# Inventory
|
|
464
|
+
|
|
465
|
+
**${Object.keys(inventory).length}** components are in ${esc(scheme.name)}. ` +
|
|
466
|
+
`The components answers picked them (see [Components](../foundations/components.md)). Anything not in is out.
|
|
467
|
+
|
|
468
|
+
` +
|
|
469
|
+
`| Component | Family | In | Built | Why |
|
|
470
|
+
| --- | --- | --- | --- | --- |
|
|
471
|
+
` +
|
|
472
|
+
[...catalogue]
|
|
473
|
+
.sort(
|
|
474
|
+
(a, b) =>
|
|
475
|
+
Number(b.id in inventory) - Number(a.id in inventory) ||
|
|
476
|
+
a.id.localeCompare(b.id),
|
|
477
|
+
)
|
|
478
|
+
.map(
|
|
479
|
+
(c) =>
|
|
480
|
+
`| ${title(c.id)} | ${c.family} | ${c.id in inventory ? "In" : "Out"} | ${built.includes(c.id) ? "Yes" : "Not yet"} | ${esc(inventory[c.id] ?? out[c.id] ?? "")} |`,
|
|
481
|
+
)
|
|
482
|
+
.join("\n") +
|
|
483
|
+
"\n";
|
|
484
|
+
// A page per component that is in the inventory and built.
|
|
485
|
+
const names = built.filter((n) => !scheme.inventory || n in inventory);
|
|
486
|
+
names.forEach((name, i) => {
|
|
487
|
+
const group = KEY_GROUP[name];
|
|
488
|
+
const keys = g.component_keys.filter((k) => k.component === group);
|
|
489
|
+
const word = new RegExp(`\\b${name.replace("-", "[ -]")}`, "i");
|
|
490
|
+
const usage = scheme.usage.filter((u) => word.test(`${u.use} ${u.not}`));
|
|
491
|
+
pages[`components/${name}.md`] =
|
|
492
|
+
fm({
|
|
493
|
+
sidebar_position: i + 1,
|
|
494
|
+
title: title(name),
|
|
495
|
+
description: `${title(name)} in ${scheme.name}.`,
|
|
496
|
+
}) +
|
|
497
|
+
NOTE +
|
|
498
|
+
`# ${title(name)}\n\n${esc(componentIntro(roots.ui, name)).replaceAll("*", "\\*").replaceAll("_", "\\_")}\n\n` +
|
|
499
|
+
`See it in [Storybook](${storybook}) under Components / ${title(name)}, in every theme.\n\n` +
|
|
500
|
+
(keys.length
|
|
501
|
+
? `## Component keys\n\nThe CSS variable is the key with \`/\` replaced by \`-\` and the base \`@\` dropped. Each key points to a token; values live only on primitives.\n\n| Key | What it controls | Points to, ${first.id} | Points to, ${dark.id} |\n| --- | --- | --- | --- |\n`
|
|
502
|
+
: `## Tokens it reads\n\nFrom shadcn/ui. It reads shadcn's variables, which theme.css maps onto this system's semantic tokens, and the radius, shadow and text scale in shadcn.css.\n`) +
|
|
503
|
+
keys
|
|
504
|
+
.map((k) => {
|
|
505
|
+
const p = k.id.replaceAll("/", ".");
|
|
506
|
+
const l = link(first, p);
|
|
507
|
+
const d = link(dark, p);
|
|
508
|
+
return `| \`${k.id}\` | ${esc(k.description)} | ${l} | ${d === l ? "same" : d} |`;
|
|
509
|
+
})
|
|
510
|
+
.join("\n") +
|
|
511
|
+
(usage.length
|
|
512
|
+
? `\n\n## When to use it\n\n${RULES}${usage.map(rule).join("\n")}`
|
|
513
|
+
: "") +
|
|
514
|
+
"\n";
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Themes
|
|
518
|
+
const brands = Object.entries(scheme.tokens.brands);
|
|
519
|
+
pages["themes.md"] =
|
|
520
|
+
fm({
|
|
521
|
+
sidebar_position: 5,
|
|
522
|
+
title: "Themes",
|
|
523
|
+
description: `Brands and modes in ${scheme.name}.`,
|
|
524
|
+
}) +
|
|
525
|
+
NOTE +
|
|
526
|
+
`# Themes\n\n${scheme.name} ships ${scheme.themes.length} themes: ${scheme.themes.map((t) => t.id).join(", ")}. Only semantic tokens change between themes; components never do.\n\n` +
|
|
527
|
+
`## How to switch\n\n- Dark mode: add the class \`dark\` to \`<html>\`.\n` +
|
|
528
|
+
(brands.length > 1
|
|
529
|
+
? `- Brand: set \`data-brand\` on \`<html>\` to ${brands.map(([b]) => `\`${b}\``).join(", ")}.\n`
|
|
530
|
+
: "") +
|
|
531
|
+
`\n## Brands\n\n${brands.map(([b, x]) => `- **${x.label}** (\`${b}\`): ${x.description}`).join("\n")}\n\n` +
|
|
532
|
+
`## Key colors in each theme\n\n| Token | ${scheme.themes.map((t) => t.id).join(" | ")} |\n| --- | ${scheme.themes.map(() => "---").join(" | ")} |\n` +
|
|
533
|
+
[
|
|
534
|
+
"color.surface.canvas",
|
|
535
|
+
"color.surface.default",
|
|
536
|
+
"color.text.primary",
|
|
537
|
+
"color.action.primary",
|
|
538
|
+
"color.border.focus",
|
|
539
|
+
]
|
|
540
|
+
.map(
|
|
541
|
+
(p) =>
|
|
542
|
+
`| \`${p}\` | ${scheme.themes.map((t) => esc(value(t, p, ""))).join(" | ")} |`,
|
|
543
|
+
)
|
|
544
|
+
.join("\n") +
|
|
545
|
+
"\n";
|
|
546
|
+
|
|
547
|
+
// For agents
|
|
548
|
+
pages["agents.md"] =
|
|
549
|
+
fm({
|
|
550
|
+
sidebar_position: 6,
|
|
551
|
+
title: "For agents",
|
|
552
|
+
description: "How an AI agent builds with this design system.",
|
|
553
|
+
}) +
|
|
554
|
+
NOTE +
|
|
555
|
+
`# For agents\n\nThis design system ships an agent layer, so an AI builds the way the system would and can say why.\n\n` +
|
|
556
|
+
`- **AGENTS.md** in the project root: the usage rules, the voice and tone, and the key decisions.\n` +
|
|
557
|
+
`- **The agent skill** in \`.claude/skills/open-ds/SKILL.md\`: how to pick, read and follow the scheme.\n` +
|
|
558
|
+
`- **The full scheme** in \`.open-ds/scheme.json\`: every answer with its reason.\n` +
|
|
559
|
+
`- **llms.txt** next to these docs: an index of these pages for agents.\n\n` +
|
|
560
|
+
`Ask the open-ds CLI or MCP server:\n\n\`\`\`bash\nopen-ds why ${scheme.id} typography.definition\nopen-ds when ${scheme.id} "a number in a table"\n\`\`\`\n`;
|
|
561
|
+
return pages;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** llms.txt for one scheme's docs: an index of its pages. */
|
|
565
|
+
export function docsLlms(scheme, pages) {
|
|
566
|
+
const md = Object.keys(pages).filter((p) => p.endsWith(".md"));
|
|
567
|
+
const href = (p) => (p === "index.md" ? "/" : `/${p.replace(/\.md$/, "")}`);
|
|
568
|
+
const head = (p) => pages[p].match(/^title: "(.*)"$/m)?.[1] ?? p;
|
|
569
|
+
return `# ${scheme.name}\n\n> ${scheme.summary}\n\n## Pages\n\n${md.map((p) => `- [${head(p)}](${href(p)})`).join("\n")}\n`;
|
|
570
|
+
}
|
package/src/guidance.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What each semantic key is for, read from the grammar's "semantics" list. Pure functions: no files.
|
|
2
|
+
|
|
3
|
+
const ids = (g) => new Map((g.semantics?.keys ?? []).map((k) => [k.id, k]));
|
|
4
|
+
|
|
5
|
+
/** The guidance id for a token path; a concept with a base resolves to its @ (color.status.danger → .@). */
|
|
6
|
+
export function guidanceId(g, path) {
|
|
7
|
+
const all = ids(g);
|
|
8
|
+
if (all.has(path)) return path;
|
|
9
|
+
if (all.has(`${path}.@`)) return `${path}.@`;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Guidance for one semantic key: its intent, uses, misuses with what to use instead, the fills it may
|
|
15
|
+
* sit on, the keys that may sit on it, and its place on the urgency order. Null when there is none.
|
|
16
|
+
*/
|
|
17
|
+
export function guidanceFor(g, path) {
|
|
18
|
+
const id = guidanceId(g, path);
|
|
19
|
+
if (!id) return null;
|
|
20
|
+
const k = ids(g).get(id);
|
|
21
|
+
const takes = (g.semantics.keys ?? [])
|
|
22
|
+
.filter((x) => x.pairs?.on.includes(id))
|
|
23
|
+
.map((x) => ({ key: x.id, contrast: x.pairs.contrast }));
|
|
24
|
+
const order = g.semantics.urgency?.order ?? [];
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
intent: k.intent,
|
|
28
|
+
use_for: k.use_for,
|
|
29
|
+
not_for: k.not_for,
|
|
30
|
+
...(k.pairs && { sits_on: k.pairs.on, contrast: k.pairs.contrast }),
|
|
31
|
+
...(k.exempt && { exempt: k.exempt }),
|
|
32
|
+
...(takes.length && { takes }),
|
|
33
|
+
...(order.includes(id) && {
|
|
34
|
+
urgency: {
|
|
35
|
+
level: order.indexOf(id) + 1,
|
|
36
|
+
of: order,
|
|
37
|
+
rule: g.semantics.urgency.description,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A copy of a scheme whose semantic tokens carry the grammar's intent as their $description. */
|
|
44
|
+
export function withGuidance(scheme, g) {
|
|
45
|
+
const all = ids(g);
|
|
46
|
+
const walk = (node, path) =>
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
Object.entries(node).map(([k, v]) => {
|
|
49
|
+
if (k.startsWith("$") || !v || typeof v !== "object") return [k, v];
|
|
50
|
+
const p = path ? `${path}.${k}` : k;
|
|
51
|
+
if ("$value" in v)
|
|
52
|
+
return [
|
|
53
|
+
k,
|
|
54
|
+
all.has(p) ? { ...v, $description: all.get(p).intent } : v,
|
|
55
|
+
];
|
|
56
|
+
return [k, walk(v, p)];
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
...scheme,
|
|
61
|
+
tokens: { ...scheme.tokens, semantic: walk(scheme.tokens.semantic, "") },
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The open-ds package as a library: the same engine the CLI, the MCP server and the site use.
|
|
2
|
+
export {
|
|
3
|
+
assetRoots,
|
|
4
|
+
focusedSkills,
|
|
5
|
+
grammar,
|
|
6
|
+
schemeIds,
|
|
7
|
+
loadScheme,
|
|
8
|
+
questions,
|
|
9
|
+
why,
|
|
10
|
+
when,
|
|
11
|
+
keyInfo,
|
|
12
|
+
} from "./schemes.mjs";
|
|
13
|
+
export {
|
|
14
|
+
componentsFor,
|
|
15
|
+
packagesFor,
|
|
16
|
+
builtComponents,
|
|
17
|
+
withImports,
|
|
18
|
+
} from "./components.mjs";
|
|
19
|
+
export { createProject } from "./create.mjs";
|
|
20
|
+
export { deploy, addComponent } from "./cli.mjs";
|