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/context.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Context on demand: one call returns only the slice an agent needs for a target, built from the
|
|
2
|
+
// scheme, the grammar and the built tokens. The CLI (open-ds context) and the MCP (get_context) share it.
|
|
3
|
+
import { guidanceFor, guidanceId } from "./guidance.mjs";
|
|
4
|
+
import {
|
|
5
|
+
assetRoots,
|
|
6
|
+
grammar,
|
|
7
|
+
keyInfo,
|
|
8
|
+
loadScheme,
|
|
9
|
+
schemeIds,
|
|
10
|
+
valuesFor,
|
|
11
|
+
when,
|
|
12
|
+
} from "./schemes.mjs";
|
|
13
|
+
|
|
14
|
+
export const TASKS = ["choose", "build", "write", "change", "review"];
|
|
15
|
+
|
|
16
|
+
const brief = (g) =>
|
|
17
|
+
(g.semantics?.keys ?? []).map((k) => ({
|
|
18
|
+
id: k.id,
|
|
19
|
+
intent: k.intent,
|
|
20
|
+
not_for: k.not_for.map((n) => `${n.when}: use ${n.use}`),
|
|
21
|
+
...(k.pairs && { sits_on: k.pairs.on, contrast: k.pairs.contrast }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const rules = (scheme, g) =>
|
|
25
|
+
g.situations.map((x) => {
|
|
26
|
+
const r = scheme.usage.find((u) => u.situation === x.id) ?? {};
|
|
27
|
+
return {
|
|
28
|
+
situation: x.id,
|
|
29
|
+
title: x.title,
|
|
30
|
+
use: r.use,
|
|
31
|
+
not: r.not,
|
|
32
|
+
why: r.why,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function task(name, scheme, g, roots) {
|
|
37
|
+
const urgency = g.semantics?.urgency;
|
|
38
|
+
const base = {
|
|
39
|
+
target: name,
|
|
40
|
+
kind: "task",
|
|
41
|
+
skill: `open-ds-${name}`,
|
|
42
|
+
scheme: scheme.id,
|
|
43
|
+
};
|
|
44
|
+
if (name === "choose")
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
read: "Pick the scheme whose use_when fits the product and whose avoid_when does not.",
|
|
48
|
+
schemes: schemeIds(roots).map((id) => {
|
|
49
|
+
const s = loadScheme(id, roots);
|
|
50
|
+
return { id, name: s.name, summary: s.summary, ...s.ai };
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
if (name === "build")
|
|
54
|
+
return {
|
|
55
|
+
...base,
|
|
56
|
+
read: "Find the situation and follow its rule. Read only component keys and semantic tokens. Check a semantic's not_for before you use it.",
|
|
57
|
+
components: g.components.map((c) => ({
|
|
58
|
+
id: c.id,
|
|
59
|
+
description: c.description,
|
|
60
|
+
keys: c.keys,
|
|
61
|
+
})),
|
|
62
|
+
rules: rules(scheme, g),
|
|
63
|
+
semantics: brief(g),
|
|
64
|
+
urgency,
|
|
65
|
+
};
|
|
66
|
+
if (name === "write")
|
|
67
|
+
return {
|
|
68
|
+
...base,
|
|
69
|
+
read: "Write in the scheme's voice. Use the words in content for common actions.",
|
|
70
|
+
voice: scheme.voice,
|
|
71
|
+
content: scheme.tokens.content,
|
|
72
|
+
};
|
|
73
|
+
if (name === "change")
|
|
74
|
+
return {
|
|
75
|
+
...base,
|
|
76
|
+
read: "Change an answer in the scheme JSON, with its why and because. Then rebuild the tokens.",
|
|
77
|
+
how_to_read: g.ai?.how_to_read,
|
|
78
|
+
answers: scheme.answers,
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
read: "Check each colour against its not_for and its pairs, each status against the urgency order, and each situation against its rule.",
|
|
83
|
+
rules: rules(scheme, g),
|
|
84
|
+
semantics: brief(g),
|
|
85
|
+
urgency,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The context for a target in a scheme. A target is a task (choose, build, write, change, review), a
|
|
91
|
+
* component (button), a component key (button/primary/background), a semantic token
|
|
92
|
+
* (color.status.danger) or a situation (status-label). Null when nothing matches.
|
|
93
|
+
*/
|
|
94
|
+
export function context(
|
|
95
|
+
schemeId,
|
|
96
|
+
target,
|
|
97
|
+
roots = assetRoots(),
|
|
98
|
+
g = grammar(roots),
|
|
99
|
+
) {
|
|
100
|
+
const scheme = loadScheme(schemeId, roots);
|
|
101
|
+
if (TASKS.includes(target)) return task(target, scheme, g, roots);
|
|
102
|
+
|
|
103
|
+
const comp = g.components.find((c) => c.id === target);
|
|
104
|
+
if (comp) {
|
|
105
|
+
const keys = comp.keys
|
|
106
|
+
.map((k) => keyInfo(schemeId, k, roots, g))
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
const sems = [...new Set(keys.map((k) => k.semantic))];
|
|
109
|
+
return {
|
|
110
|
+
target,
|
|
111
|
+
kind: "component",
|
|
112
|
+
scheme: scheme.id,
|
|
113
|
+
description: comp.description,
|
|
114
|
+
keys: keys.map(({ guidance, ...k }) => k),
|
|
115
|
+
guidance: sems.map((p) => guidanceFor(g, p)).filter(Boolean),
|
|
116
|
+
rules: rules(scheme, g).filter((r) =>
|
|
117
|
+
`${r.use} ${r.not}`.toLowerCase().includes(target),
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const key = target.includes("/") && keyInfo(schemeId, target, roots, g);
|
|
123
|
+
if (key) return { target, kind: "component key", scheme: scheme.id, ...key };
|
|
124
|
+
|
|
125
|
+
const id = guidanceId(g, target);
|
|
126
|
+
if (id)
|
|
127
|
+
return {
|
|
128
|
+
target,
|
|
129
|
+
kind: "semantic",
|
|
130
|
+
scheme: scheme.id,
|
|
131
|
+
guidance: guidanceFor(g, id),
|
|
132
|
+
values: valuesFor(schemeId, id, roots),
|
|
133
|
+
read_by: g.component_keys
|
|
134
|
+
.filter((k) => k.default === id)
|
|
135
|
+
.map((k) => k.id),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (g.situations.some((x) => x.id === target)) {
|
|
139
|
+
const rule = when(scheme, target, g);
|
|
140
|
+
return { target, kind: "situation", scheme: scheme.id, ...rule };
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
package/src/create.mjs
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Writes a full design system project for one scheme: its own Style Dictionary token build,
|
|
2
|
+
// the components, Storybook, Docusaurus docs, the answer map, the agent files and CI.
|
|
3
|
+
// The project depends on the open-ds package for why, when and add.
|
|
4
|
+
|
|
5
|
+
import { themeName } from "./tokens.mjs";
|
|
6
|
+
import { withGuidance } from "./guidance.mjs";
|
|
7
|
+
import { tokenReadmes } from "./readmes.mjs";
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { agentsMd, skillSection } from "./agents.mjs";
|
|
18
|
+
import { componentsFor, packagesFor } from "./components.mjs";
|
|
19
|
+
import { docsLlms, docsPages } from "./docs.mjs";
|
|
20
|
+
import { assetRoots, focusedSkills, grammar, loadScheme } from "./schemes.mjs";
|
|
21
|
+
|
|
22
|
+
const VERSION = JSON.parse(
|
|
23
|
+
readFileSync(
|
|
24
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
25
|
+
"utf8",
|
|
26
|
+
),
|
|
27
|
+
).version;
|
|
28
|
+
|
|
29
|
+
const DEV_DEPENDENCIES = {
|
|
30
|
+
"@docusaurus/core": "^3.10.2",
|
|
31
|
+
"@docusaurus/preset-classic": "^3.10.2",
|
|
32
|
+
"@docusaurus/types": "^3.10.2",
|
|
33
|
+
"@mdx-js/react": "^3.1.0",
|
|
34
|
+
"@storybook/addon-docs": "^10.6.5",
|
|
35
|
+
"@storybook/react-vite": "^10.6.5",
|
|
36
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
37
|
+
"@types/react": "^19.1.13",
|
|
38
|
+
"@types/react-dom": "^19.1.9",
|
|
39
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
40
|
+
"prism-react-renderer": "^2.4.1",
|
|
41
|
+
storybook: "^10.6.5",
|
|
42
|
+
"style-dictionary": "^5.7.1",
|
|
43
|
+
tailwindcss: "^4.3.3",
|
|
44
|
+
typescript: "^5.9.2",
|
|
45
|
+
vite: "^8.3.5",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const json = (data) => JSON.stringify(data, null, 2) + "\n";
|
|
49
|
+
|
|
50
|
+
const walk = (dir) =>
|
|
51
|
+
readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
52
|
+
e.isDirectory() ? walk(join(dir, e.name)) : [join(dir, e.name)],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function readme(scheme, name, brands) {
|
|
56
|
+
return [
|
|
57
|
+
`# ${name}`,
|
|
58
|
+
"",
|
|
59
|
+
`A design system built from the Open DS scheme **${scheme.name}** (${scheme.id} ${scheme.version}). ${scheme.summary}`,
|
|
60
|
+
"",
|
|
61
|
+
"## Run it",
|
|
62
|
+
"",
|
|
63
|
+
"```bash",
|
|
64
|
+
"npm install",
|
|
65
|
+
"npm run storybook # the components, at http://localhost:6006",
|
|
66
|
+
"npm run docs # the docs, at http://localhost:3000",
|
|
67
|
+
"```",
|
|
68
|
+
"",
|
|
69
|
+
"`npm run build` builds the tokens, type-checks the components and builds Storybook and the docs.",
|
|
70
|
+
"",
|
|
71
|
+
"## What is here",
|
|
72
|
+
"",
|
|
73
|
+
"| Folder | What it holds |",
|
|
74
|
+
"| --- | --- |",
|
|
75
|
+
"| `tokens/src/` | The DTCG token source, one file per group: `primitive/` (steps only: colour by hue with light and dark ramps; space, size, radius, font, shadow, motion), `theme/` (each brand's decisions per mode, on the semantic paths, with a base `@` per concept), `semantic/`, `component/` and content. Each folder has a README. Only semantic and component keys ship as CSS variables. |",
|
|
76
|
+
"| `tokens/build.mjs` | The Style Dictionary build. `npm run tokens` writes CSS, the shadcn and Tailwind theme and JSON to `build/`. |",
|
|
77
|
+
"| `src/components/ui/` | The components. Each reads only component keys such as `--button-primary-background`. |",
|
|
78
|
+
"| `src/styles/index.css` | Tailwind, the built tokens, the theme and the base styles. |",
|
|
79
|
+
"| `stories/` | Storybook stories, with the usage rules next to each component. |",
|
|
80
|
+
"| `docs/` | The Docusaurus docs: reasons, voice, foundations, components, themes. |",
|
|
81
|
+
"| `scheme.json` | The answer map: every decision with why and because, every usage rule, the voice. |",
|
|
82
|
+
"| `AGENTS.md`, `.claude/skills/` | What a coding agent reads before it writes UI. |",
|
|
83
|
+
"",
|
|
84
|
+
`Themes: ${scheme.themes.map((t) => t.id).join(", ")}.${brands.length > 1 ? ` Set \`data-brand\` on \`<html>\` to pick a brand.` : ""} Add the class \`dark\` for dark mode.`,
|
|
85
|
+
"",
|
|
86
|
+
"## Ask why",
|
|
87
|
+
"",
|
|
88
|
+
"```bash",
|
|
89
|
+
"npm run why -- typography.definition",
|
|
90
|
+
"npm run when -- destructive action",
|
|
91
|
+
"```",
|
|
92
|
+
"",
|
|
93
|
+
"## Built on open source",
|
|
94
|
+
"",
|
|
95
|
+
"This design system is built on open-source projects, used as their authors intend and under their own licences:",
|
|
96
|
+
"[shadcn/ui](https://ui.shadcn.com) (the component patterns), [Radix UI](https://www.radix-ui.com) (accessible primitives),",
|
|
97
|
+
"[Tailwind CSS](https://tailwindcss.com), [Style Dictionary](https://styledictionary.com) (the token build),",
|
|
98
|
+
"[Storybook](https://storybook.js.org), [Docusaurus](https://docusaurus.io), [TanStack Table](https://tanstack.com/table)",
|
|
99
|
+
"and [Lucide](https://lucide.dev) icons. Each package's licence is in `node_modules`.",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Writes a design system project for a scheme into dir. Returns what it wrote. */
|
|
105
|
+
export function createProject(
|
|
106
|
+
schemeId,
|
|
107
|
+
dir,
|
|
108
|
+
{
|
|
109
|
+
brand,
|
|
110
|
+
openDs = `^${VERSION}`,
|
|
111
|
+
overwrite = false,
|
|
112
|
+
roots = assetRoots(),
|
|
113
|
+
} = {},
|
|
114
|
+
) {
|
|
115
|
+
const scheme = loadScheme(schemeId, roots);
|
|
116
|
+
const g = grammar(roots);
|
|
117
|
+
const brands = Object.keys(scheme.tokens.brands);
|
|
118
|
+
if (brand && !brands.includes(brand))
|
|
119
|
+
throw new Error(
|
|
120
|
+
`${scheme.name} has no brand "${brand}". Brands: ${brands.join(", ")}.`,
|
|
121
|
+
);
|
|
122
|
+
if (!existsSync(join(roots.tokens, schemeId, "tokens.css")))
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Tokens for ${schemeId} are not built. Run: pnpm --dir tokens build`,
|
|
125
|
+
);
|
|
126
|
+
// A fresh git folder, a vendored package and npm's leftovers do not count as content.
|
|
127
|
+
const SETUP = new Set([
|
|
128
|
+
".git",
|
|
129
|
+
".DS_Store",
|
|
130
|
+
"vendor",
|
|
131
|
+
"node_modules",
|
|
132
|
+
"package-lock.json",
|
|
133
|
+
]);
|
|
134
|
+
const taken = existsSync(dir)
|
|
135
|
+
? readdirSync(dir).filter((f) => !SETUP.has(f))
|
|
136
|
+
: [];
|
|
137
|
+
if (taken.length && !overwrite) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${dir} is not empty (${taken.slice(0, 5).join(", ")}). Run create again with --yes to write into it.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const name = basename(dir);
|
|
143
|
+
const files = [];
|
|
144
|
+
const write = (rel, text) => {
|
|
145
|
+
mkdirSync(dirname(join(dir, rel)), { recursive: true });
|
|
146
|
+
writeFileSync(join(dir, rel), text);
|
|
147
|
+
files.push(rel);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// The fixed parts: token build, Storybook, Docusaurus config, CI. "gitignore" becomes .gitignore,
|
|
151
|
+
// because npm drops .gitignore files from packages.
|
|
152
|
+
const template = join(roots.templates, "project");
|
|
153
|
+
for (const path of walk(template)) {
|
|
154
|
+
const rel = relative(template, path);
|
|
155
|
+
write(rel === "gitignore" ? ".gitignore" : rel, readFileSync(path, "utf8"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The token source, as DTCG files the team owns and edits: one file per group, in folders.
|
|
159
|
+
// Semantic descriptions come from the grammar's guidance, written once for every scheme.
|
|
160
|
+
const t = withGuidance(scheme, g).tokens;
|
|
161
|
+
const groups = (tree) =>
|
|
162
|
+
Object.entries(tree).filter(([k]) => !k.startsWith("$"));
|
|
163
|
+
// Primitives: color/<hue>, font/<part>, motion/<duration|easing>, and one file for each other group.
|
|
164
|
+
for (const [group, node] of groups(t.primitive)) {
|
|
165
|
+
if (group === "color" || group === "font")
|
|
166
|
+
for (const [part, sub] of groups(node))
|
|
167
|
+
write(
|
|
168
|
+
`tokens/src/primitive/${group}/${part}.tokens.json`,
|
|
169
|
+
json({ [group]: { [part]: sub } }),
|
|
170
|
+
);
|
|
171
|
+
else if (group === "duration" || group === "easing")
|
|
172
|
+
write(
|
|
173
|
+
`tokens/src/primitive/motion/${group}.tokens.json`,
|
|
174
|
+
json({ [group]: node }),
|
|
175
|
+
);
|
|
176
|
+
else
|
|
177
|
+
write(
|
|
178
|
+
`tokens/src/primitive/${group}.tokens.json`,
|
|
179
|
+
json({ [group]: node }),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
// Themes: one file per brand, with its base, light and dark picks.
|
|
183
|
+
for (const b of brands)
|
|
184
|
+
write(
|
|
185
|
+
`tokens/src/theme/${b}.tokens.json`,
|
|
186
|
+
json({ [themeName(scheme.id, b)]: { theme: t.brands[b].theme } }),
|
|
187
|
+
);
|
|
188
|
+
// Semantics and component keys: one file per group.
|
|
189
|
+
for (const [group, node] of groups(t.semantic))
|
|
190
|
+
write(`tokens/src/semantic/${group}.tokens.json`, json({ [group]: node }));
|
|
191
|
+
for (const [group, node] of groups(t.component))
|
|
192
|
+
write(`tokens/src/component/${group}.tokens.json`, json({ [group]: node }));
|
|
193
|
+
if (t.content)
|
|
194
|
+
write("tokens/src/content.tokens.json", json({ content: t.content }));
|
|
195
|
+
// Every folder says what it holds and its rules, for people and agents.
|
|
196
|
+
for (const [dir, text] of Object.entries(tokenReadmes(scheme)))
|
|
197
|
+
write(`tokens/src/${dir}README.md`, text);
|
|
198
|
+
// The default brand goes first, so it is the one on :root.
|
|
199
|
+
const order = brand ? [brand, ...brands.filter((b) => b !== brand)] : brands;
|
|
200
|
+
write(
|
|
201
|
+
"tokens/themes.json",
|
|
202
|
+
json({
|
|
203
|
+
description:
|
|
204
|
+
"The brands and themes tokens/build.mjs builds. The first brand is the default, on :root. theme is the name its theme tokens carry: --<theme>-theme-light-<key>.",
|
|
205
|
+
name: scheme.name,
|
|
206
|
+
brands: order.map((b) => ({
|
|
207
|
+
id: b,
|
|
208
|
+
label: t.brands[b].label ?? b,
|
|
209
|
+
theme: themeName(scheme.id, b),
|
|
210
|
+
})),
|
|
211
|
+
themes: order.flatMap((b) => scheme.themes.filter((x) => x.brand === b)),
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// The scheme's inventory, plus the components those import, and their styles.
|
|
216
|
+
const components = componentsFor(scheme, roots);
|
|
217
|
+
for (const c of components) {
|
|
218
|
+
write(
|
|
219
|
+
`src/components/ui/${c}.tsx`,
|
|
220
|
+
readFileSync(join(roots.ui, "components", `${c}.tsx`), "utf8").replaceAll(
|
|
221
|
+
`from "../lib/`,
|
|
222
|
+
`from "../../lib/`,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
for (const lib of readdirSync(join(roots.ui, "lib")))
|
|
227
|
+
write(`src/lib/${lib}`, readFileSync(join(roots.ui, "lib", lib), "utf8"));
|
|
228
|
+
write(
|
|
229
|
+
"src/styles/base.css",
|
|
230
|
+
readFileSync(join(roots.ui, "styles.css"), "utf8"),
|
|
231
|
+
);
|
|
232
|
+
write(
|
|
233
|
+
"src/index.ts",
|
|
234
|
+
`// ${scheme.name} components. Import "./styles/index.css" once in an app.\n` +
|
|
235
|
+
components
|
|
236
|
+
.map((c) => `export * from "./components/ui/${c}";`)
|
|
237
|
+
.join("\n") +
|
|
238
|
+
"\n",
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Storybook stories, with this scheme's usage rules.
|
|
242
|
+
// A story is copied only when everything it imports from the components is installed here.
|
|
243
|
+
const exported = new Set(
|
|
244
|
+
components.flatMap((c) => {
|
|
245
|
+
const src = readFileSync(
|
|
246
|
+
join(roots.ui, "components", `${c}.tsx`),
|
|
247
|
+
"utf8",
|
|
248
|
+
);
|
|
249
|
+
const names = [
|
|
250
|
+
...src.matchAll(
|
|
251
|
+
/^export (?:function|const|interface|type) ([A-Za-z]+)/gm,
|
|
252
|
+
),
|
|
253
|
+
].map((m) => m[1]);
|
|
254
|
+
for (const m of src.matchAll(/^export \{([^}]+)\}/gm))
|
|
255
|
+
names.push(
|
|
256
|
+
...m[1]
|
|
257
|
+
.split(",")
|
|
258
|
+
.map((x) => x.trim())
|
|
259
|
+
.filter(Boolean),
|
|
260
|
+
);
|
|
261
|
+
return names;
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
const storyFits = (text) => {
|
|
265
|
+
const m = text.match(/import \{([^}]+)\} from "@open-ds\/ui"/);
|
|
266
|
+
return (
|
|
267
|
+
!m ||
|
|
268
|
+
m[1]
|
|
269
|
+
.split(",")
|
|
270
|
+
.map((x) => x.trim())
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.every((n) => exported.has(n))
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
for (const f of readdirSync(roots.stories).filter(
|
|
276
|
+
(x) =>
|
|
277
|
+
x.endsWith(".stories.tsx") &&
|
|
278
|
+
storyFits(readFileSync(join(roots.stories, x), "utf8")),
|
|
279
|
+
)) {
|
|
280
|
+
write(
|
|
281
|
+
`stories/${f}`,
|
|
282
|
+
readFileSync(join(roots.stories, f), "utf8")
|
|
283
|
+
.replaceAll(`from "@open-ds/ui"`, `from "../src"`)
|
|
284
|
+
.replaceAll(`from "../src/usage"`, `from "./usage"`)
|
|
285
|
+
.replaceAll(`from "../src/samples"`, `from "./samples"`)
|
|
286
|
+
.replaceAll(`from "../src/docs-blocks"`, `from "./docs-blocks"`),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The answer map and the situations the usage rules name.
|
|
291
|
+
write("scheme.json", json(scheme));
|
|
292
|
+
write(
|
|
293
|
+
".open-ds/grammar.json",
|
|
294
|
+
json({
|
|
295
|
+
situations: g.situations,
|
|
296
|
+
catalogue: g.catalogue,
|
|
297
|
+
component_keys: g.component_keys,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
write(
|
|
301
|
+
".open-ds/situations.json",
|
|
302
|
+
json(
|
|
303
|
+
g.situations.map((s) => ({
|
|
304
|
+
id: s.id,
|
|
305
|
+
title: s.title,
|
|
306
|
+
description: s.description,
|
|
307
|
+
})),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// The docs pages.
|
|
312
|
+
const pages = docsPages(scheme, g, roots);
|
|
313
|
+
for (const [rel, text] of Object.entries(pages))
|
|
314
|
+
write(`docs/docs/${rel}`, text);
|
|
315
|
+
write("docs/static/llms.txt", docsLlms(scheme, pages));
|
|
316
|
+
|
|
317
|
+
// The agent files.
|
|
318
|
+
const defaultBrand = brands.length > 1 ? order[0] : null;
|
|
319
|
+
write(
|
|
320
|
+
"AGENTS.md",
|
|
321
|
+
agentsMd(scheme, g, { brand: defaultBrand, components, project: true }),
|
|
322
|
+
);
|
|
323
|
+
write(
|
|
324
|
+
".claude/skills/open-ds/SKILL.md",
|
|
325
|
+
readFileSync(join(roots.skills, "open-ds", "SKILL.md"), "utf8") +
|
|
326
|
+
skillSection(scheme, { project: true }),
|
|
327
|
+
);
|
|
328
|
+
for (const skill of focusedSkills(roots))
|
|
329
|
+
write(
|
|
330
|
+
`.claude/skills/${skill}/SKILL.md`,
|
|
331
|
+
readFileSync(join(roots.skills, skill, "SKILL.md"), "utf8"),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
write(
|
|
335
|
+
"package.json",
|
|
336
|
+
json({
|
|
337
|
+
name,
|
|
338
|
+
version: "0.1.0",
|
|
339
|
+
private: true,
|
|
340
|
+
description: `${scheme.name}: a design system built from the Open DS scheme ${scheme.id}.`,
|
|
341
|
+
main: "src/index.ts",
|
|
342
|
+
scripts: {
|
|
343
|
+
tokens: "node tokens/build.mjs",
|
|
344
|
+
typecheck: "npm run tokens && tsc --noEmit",
|
|
345
|
+
storybook: "npm run tokens && storybook dev -p 6006",
|
|
346
|
+
"build-storybook":
|
|
347
|
+
"npm run tokens && storybook build -o storybook-static --quiet",
|
|
348
|
+
docs: "npm run tokens && docusaurus start docs",
|
|
349
|
+
"build-docs": "npm run tokens && docusaurus build docs",
|
|
350
|
+
build:
|
|
351
|
+
"npm run typecheck && npm run build-storybook && npm run build-docs",
|
|
352
|
+
why: `open-ds why ${scheme.id}`,
|
|
353
|
+
when: `open-ds when ${scheme.id}`,
|
|
354
|
+
},
|
|
355
|
+
dependencies: {
|
|
356
|
+
react: "^19.1.1",
|
|
357
|
+
"react-dom": "^19.1.1",
|
|
358
|
+
...packagesFor(components, roots),
|
|
359
|
+
},
|
|
360
|
+
devDependencies: { "open-ds": openDs, ...DEV_DEPENDENCIES },
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
write("README.md", readme(scheme, name, brands));
|
|
364
|
+
|
|
365
|
+
const record = {
|
|
366
|
+
$comment:
|
|
367
|
+
"Written by open-ds create. The scheme this design system was built from.",
|
|
368
|
+
kind: "project",
|
|
369
|
+
scheme: scheme.id,
|
|
370
|
+
version: scheme.version,
|
|
371
|
+
brand: defaultBrand,
|
|
372
|
+
components,
|
|
373
|
+
files: files.sort(),
|
|
374
|
+
};
|
|
375
|
+
writeFileSync(join(dir, "open-ds.json"), json(record));
|
|
376
|
+
return record;
|
|
377
|
+
}
|