@codepress/design-kit 0.1.1 → 0.1.3
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 +55 -3
- package/package.json +2 -1
- package/skills/design-kit-setup/SKILL.md +136 -0
- package/src/capture-pages.mjs +22 -5
- package/src/cli.mjs +31 -1
- package/src/init.mjs +1017 -0
- package/src/templates/design-kit-skill.md +156 -0
package/README.md
CHANGED
|
@@ -19,6 +19,47 @@ It runs on Node 20 or later. Playwright is a peer dependency, so the capture
|
|
|
19
19
|
drives the Playwright and Chromium your project already has. The `kit`
|
|
20
20
|
commands need neither.
|
|
21
21
|
|
|
22
|
+
## Set up in your repository
|
|
23
|
+
|
|
24
|
+
`design-kit init` sets a kit up as one reviewable change. Run it in the
|
|
25
|
+
repository's root, or in a monorepo with `--root <package>` (the config goes
|
|
26
|
+
there; the workflow, skill and instructions line go at the repository root):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @codepress/design-kit init --dry-run # what it found and would write
|
|
30
|
+
npx @codepress/design-kit init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
It looks for Storybook (a `storybook` or `@storybook/*` dependency, or a
|
|
34
|
+
`.storybook` folder, in the project or its workspace packages) and its
|
|
35
|
+
version, the framework (Next.js, Vite, Remix, Create React App, Astro or
|
|
36
|
+
other), the package manager and lockfile, the dev or start script and the
|
|
37
|
+
app's local URL, an existing Playwright, and whether the app switches to dark
|
|
38
|
+
with a class or a `data-theme` attribute. Then it writes:
|
|
39
|
+
|
|
40
|
+
- `design-kit.config.json`: the `stories` section when Storybook is found, a
|
|
41
|
+
`pages` section with `TODO` selectors to fill in, and the `theme`.
|
|
42
|
+
- `.github/workflows/design-kit.yml`: a daily build (and `workflow_dispatch`)
|
|
43
|
+
on `ubuntu-latest`. It skips when there are no commits since the newest
|
|
44
|
+
`design-kit` artifact's commit; otherwise it installs dependencies, this
|
|
45
|
+
package at a pinned version and Chromium, builds Storybook and captures the
|
|
46
|
+
stories, captures the pages once no `TODO` is left in the config, and
|
|
47
|
+
uploads the `design-kit` artifact for 30 days. A run with nothing to
|
|
48
|
+
capture yet (no stories, pages still `TODO`) ends with a warning and
|
|
49
|
+
uploads nothing. The page capture gets
|
|
50
|
+
`DESIGN_KIT_USERNAME` and `DESIGN_KIT_PASSWORD` from repository secrets for
|
|
51
|
+
a sign-in module (`pages.auth.setup`).
|
|
52
|
+
- `.claude/skills/design-kit/SKILL.md`: how agents fetch the newest kit,
|
|
53
|
+
compose mockups from it and caption them, and `.agents/skills/design-kit` as
|
|
54
|
+
a symlink to it (a copy when the repository keeps real folders there).
|
|
55
|
+
- One line in `AGENTS.md`, or `CLAUDE.md` when only that exists (a new
|
|
56
|
+
`AGENTS.md` when neither does), pointing at the skill.
|
|
57
|
+
|
|
58
|
+
It prints what it found and what is still to decide: the page selectors, the
|
|
59
|
+
start command, sign-in, a missing Storybook. It never overwrites an existing
|
|
60
|
+
file without `--force` (a repository with a `design-kit.config.json` exits 1,
|
|
61
|
+
"already set up") and never writes a secret.
|
|
62
|
+
|
|
22
63
|
## Configuration
|
|
23
64
|
|
|
24
65
|
The commands read `design-kit.config.json` from the working directory or the
|
|
@@ -109,8 +150,9 @@ export returns (`async ({ browser, baseUrl, config }) => state`).
|
|
|
109
150
|
for apps that render a different frame at phone width; `layoutSlots[<that
|
|
110
151
|
value>]` then replaces `slots`.
|
|
111
152
|
- `forbidStrings`: values that must never reach the kit, such as a test
|
|
112
|
-
account's token.
|
|
113
|
-
HTML or JSON, or (for a token) any long segment of it,
|
|
153
|
+
account's token. When any page's output contains one, as written, escaped
|
|
154
|
+
for HTML or JSON, or (for a token) any long segment of it, the capture
|
|
155
|
+
fails (exit 1) and writes no page, so a CI job uploads nothing.
|
|
114
156
|
- `fontsPath`, `copyFonts`: the fonts the pages load are copied into the
|
|
115
157
|
kit's `fonts/` folder (`copyFonts: false` keeps them out); `fontsPath` says
|
|
116
158
|
which URL path holds them when not every font file is one.
|
|
@@ -122,21 +164,31 @@ value>]` then replaces `slots`.
|
|
|
122
164
|
## Commands
|
|
123
165
|
|
|
124
166
|
```bash
|
|
167
|
+
design-kit init [--root dir] [--dry-run] [--force]
|
|
125
168
|
design-kit capture stories [--config f] [--out dir] [--storybook-dir dir]
|
|
126
169
|
[--only id,... | --only-file f] [--base previous-kit]
|
|
127
170
|
design-kit capture pages [--config f] [--out dir] [--base-url url] [--only name,...]
|
|
128
171
|
[--ready name=selector] [--screenshots dir] [--timeout ms]
|
|
129
172
|
design-kit kit info|find|show|compose ... [--kit dir]
|
|
130
173
|
design-kit validate [--config f]
|
|
174
|
+
design-kit setup-guide
|
|
131
175
|
```
|
|
132
176
|
|
|
177
|
+
`setup-guide` prints the agent skill for setting a kit up in a repository
|
|
178
|
+
that has none, as one reviewable pull request: run `init`, decide between
|
|
179
|
+
Storybook stories and page capture, wire sign-in through repository secrets,
|
|
180
|
+
build the kit once and report what it captured. An agent without the skill
|
|
181
|
+
installed can run `npx --yes @codepress/design-kit@0.1.3 setup-guide` and
|
|
182
|
+
follow what it prints.
|
|
183
|
+
|
|
133
184
|
`--only` re-captures just those stories (Storybook ids, with or without the
|
|
134
185
|
`story:` prefix and width) and copies every other row from `--base`, byte for
|
|
135
186
|
byte. The two captures share one kit folder and keep each other's files: run
|
|
136
187
|
both with the same `--out`.
|
|
137
188
|
|
|
138
189
|
Exit codes: 0 done, 1 a capture could not run (no Storybook build, the app
|
|
139
|
-
did not start
|
|
190
|
+
did not start, a forbidden string) or `init` found the repository already set
|
|
191
|
+
up, 2 a config or usage error. A story or page that fails is a
|
|
140
192
|
`gaps.json` row and does not fail the run.
|
|
141
193
|
|
|
142
194
|
Operational knobs: `DESIGN_KIT_CONCURRENCY` (browser pages at once, default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codepress/design-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Capture your Storybook and real app pages as a static design kit, and compose self-contained mockups from it",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"bin/",
|
|
25
25
|
"src/",
|
|
26
26
|
"schema/",
|
|
27
|
+
"skills/",
|
|
27
28
|
"README.md",
|
|
28
29
|
"LICENSE"
|
|
29
30
|
],
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-kit-setup
|
|
3
|
+
description: "Set a design kit up in a repository that has none, as one reviewable pull request: run `npx @codepress/design-kit init`, fill in the pages it captures, decide with the person whether to add Storybook stories or capture real pages, wire sign-in through repository secrets, build the kit once locally, and report what it captured. Use when asked to set up a design kit, add design-kit to a repository, or make mockups use the app's real components."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Set up a design kit
|
|
7
|
+
|
|
8
|
+
A design kit is the app's real markup for mockups: every Storybook story and
|
|
9
|
+
the app's main pages, captured as static HTML with their CSS and fonts by
|
|
10
|
+
`@codepress/design-kit`. Agents compose mockups from it, so the frame and the
|
|
11
|
+
existing components are real and only the new parts are drawn by hand.
|
|
12
|
+
|
|
13
|
+
Your job is **one reviewable pull request** that sets a kit up: a config, a
|
|
14
|
+
daily GitHub Actions build that uploads the kit, and a `design-kit` skill (plus
|
|
15
|
+
one line in `AGENTS.md` or `CLAUDE.md`) so the repository's agents use it. You
|
|
16
|
+
never merge it.
|
|
17
|
+
|
|
18
|
+
## 1. Run `init`
|
|
19
|
+
|
|
20
|
+
From the repository root (or with `--root <subpackage>` when the app lives in
|
|
21
|
+
one package of a monorepo):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx --yes @codepress/design-kit init --dry-run # what it found and would write
|
|
25
|
+
npx --yes @codepress/design-kit init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It detects Storybook and its version, the framework, the package manager, the
|
|
29
|
+
dev script, Playwright and the dark-theme mechanism, then writes:
|
|
30
|
+
|
|
31
|
+
- `design-kit.config.json`: the stories (when Storybook is found), a `pages`
|
|
32
|
+
skeleton with `TODO` selectors, and the theme;
|
|
33
|
+
- `.github/workflows/design-kit.yml`: a daily build (and manual dispatch) that
|
|
34
|
+
skips when nothing was committed since the last kit, and uploads the
|
|
35
|
+
`design-kit` artifact for 30 days;
|
|
36
|
+
- `.claude/skills/design-kit/SKILL.md` and `.agents/skills/design-kit`: how
|
|
37
|
+
agents fetch the kit, compose mockups and caption them;
|
|
38
|
+
- one line in `AGENTS.md` (or `CLAUDE.md`) pointing at that skill.
|
|
39
|
+
|
|
40
|
+
It never overwrites a file: a repository that already has
|
|
41
|
+
`design-kit.config.json` exits 1 with "already set up". Stop there and tell the
|
|
42
|
+
person a kit is already configured; replace it with `--force` only when they ask.
|
|
43
|
+
|
|
44
|
+
Read the summary it prints. Its **Still to decide** list is your checklist for
|
|
45
|
+
the rest of this skill.
|
|
46
|
+
|
|
47
|
+
## 2. Storybook or pages: the judgment calls
|
|
48
|
+
|
|
49
|
+
**Storybook present.** Keep the `stories` section `init` wrote and capture
|
|
50
|
+
every story. Then add pages when the app has a dev server that can be reached
|
|
51
|
+
locally (step 3); a kit with pages has frames that new content can go into.
|
|
52
|
+
|
|
53
|
+
**No Storybook.** Propose one of these to the person, through the Orchestrator
|
|
54
|
+
when you work for one, in a short message with your recommendation:
|
|
55
|
+
|
|
56
|
+
- (a) **add Storybook** with stories for the components the main screens use,
|
|
57
|
+
covering their default, empty, loading, error and long-content states. For
|
|
58
|
+
React with Vite or Next.js, `npx storybook@8 init` sets it up; write the
|
|
59
|
+
stories next to the components;
|
|
60
|
+
- (b) **capture real pages** from the dev server (step 3);
|
|
61
|
+
- (c) **both**.
|
|
62
|
+
|
|
63
|
+
When nobody answers, do (b), plus (a) for components that are clearly reusable
|
|
64
|
+
(buttons, inputs, cards, dialogs, tables that several screens share). Say in the
|
|
65
|
+
pull request which you did and why.
|
|
66
|
+
|
|
67
|
+
**Supported first:** Storybook 7 and 8 static builds of any renderer, page
|
|
68
|
+
capture of any URL, and new stories for React only. For another stack (Vue,
|
|
69
|
+
Svelte, Angular, an older Storybook), capture pages and say honestly in the
|
|
70
|
+
pull request that new stories were not written, and why.
|
|
71
|
+
|
|
72
|
+
## 3. Fill in the pages
|
|
73
|
+
|
|
74
|
+
Replace the `TODO`s in `pages`:
|
|
75
|
+
|
|
76
|
+
- `startCommand` and `waitUrl`: the command that serves the app and a URL that
|
|
77
|
+
answers once it is up. `init` guessed them from the dev script, a Playwright
|
|
78
|
+
`baseURL` or the framework's default port; check them.
|
|
79
|
+
- `pages[]`: the app's main screens, usually three to eight (home or
|
|
80
|
+
dashboard, a list, a detail, settings). For each, `name` (lowercase, the
|
|
81
|
+
kit's `route:<name>` id), `path`, `ready` (a selector that shows once the
|
|
82
|
+
page has its real content, not a spinner) and `slots.main` (the selector of
|
|
83
|
+
the main content region that mockups replace, such as `main`). Add other
|
|
84
|
+
slots, such as `list` for a side list, where a screen has them.
|
|
85
|
+
- `shellReady`: a selector the app's frame shows before any page content,
|
|
86
|
+
when it has one.
|
|
87
|
+
- `forbidStrings`: values that must never reach the kit, such as a test
|
|
88
|
+
account's token or real customer data the pages might render. A page whose
|
|
89
|
+
markup carries one fails the whole capture, and nothing is uploaded.
|
|
90
|
+
|
|
91
|
+
The workflow skips page capture while any `TODO` is left.
|
|
92
|
+
|
|
93
|
+
## 4. Signing in
|
|
94
|
+
|
|
95
|
+
When the pages need a signed-in user:
|
|
96
|
+
|
|
97
|
+
- Use a **seeded test user** or the app's **mock mode** (a mock API, fixture
|
|
98
|
+
data, a demo flag), never a real person's account.
|
|
99
|
+
- Sign in with a Playwright `storageState`, produced in CI: write a small
|
|
100
|
+
module named in `pages.auth.setup` whose default export signs in
|
|
101
|
+
(`async ({ browser, baseUrl }) => state`) with credentials it reads from the
|
|
102
|
+
environment. The workflow passes `DESIGN_KIT_USERNAME` and
|
|
103
|
+
`DESIGN_KIT_PASSWORD` from repository secrets to the page capture.
|
|
104
|
+
- **Never commit a credential**, a token or a saved storage state. Say in the
|
|
105
|
+
pull request exactly which secrets the person must add (Settings → Secrets
|
|
106
|
+
and variables → Actions) and what they are for.
|
|
107
|
+
|
|
108
|
+
## 5. Build once locally
|
|
109
|
+
|
|
110
|
+
Before opening the pull request, build the kit once with the commands in the
|
|
111
|
+
new `.claude/skills/design-kit/SKILL.md` ("When there is no kit"), into a
|
|
112
|
+
scratch folder outside the repository, and check it:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
node "$KIT/kit.mjs" info # counts: stories, pages, failures
|
|
116
|
+
cat "$KIT/gaps.json" # what failed, and why
|
|
117
|
+
node "$KIT/kit.mjs" compose --frame route:<page>@1280 --out "$TMPDIR/frame.html"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Open one composed frame in a browser and take a screenshot. Fix what you can
|
|
121
|
+
(a wrong `ready` selector, a page that needs data) and run it again. Never
|
|
122
|
+
commit the kit itself.
|
|
123
|
+
|
|
124
|
+
## 6. Open the pull request
|
|
125
|
+
|
|
126
|
+
The description says:
|
|
127
|
+
|
|
128
|
+
- what `init` found (Storybook and version, framework, package manager);
|
|
129
|
+
- what the kit captured: the number of stories and pages, and every gap with
|
|
130
|
+
its reason;
|
|
131
|
+
- the screenshot of one composed frame;
|
|
132
|
+
- the secrets to add, if any, and the choice made in step 2;
|
|
133
|
+
- anything unsupported on this stack, said plainly.
|
|
134
|
+
|
|
135
|
+
After it merges, the workflow's first scheduled or manual run uploads the
|
|
136
|
+
kit, and the repository's agents start composing mockups from it.
|
package/src/capture-pages.mjs
CHANGED
|
@@ -376,8 +376,16 @@ function themeClassesOf(theme) {
|
|
|
376
376
|
* browser}: set `interrupted` to stop between pages; `browser` is set once
|
|
377
377
|
* launched so a signal handler can close it).
|
|
378
378
|
*
|
|
379
|
-
* Returns 0, or 130 when interrupted (nothing is written then).
|
|
379
|
+
* Returns 0, or 130 when interrupted (nothing is written then). Throws,
|
|
380
|
+
* writing nothing, when any page's markup carries a forbidStrings value.
|
|
380
381
|
*/
|
|
382
|
+
/** A page's markup carries a forbidStrings value. */
|
|
383
|
+
class ForbiddenStringError extends Error {
|
|
384
|
+
constructor() {
|
|
385
|
+
super("the page's markup carries a forbidStrings value");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
381
389
|
export async function capturePages(config, options = {}) {
|
|
382
390
|
const settings = config.pages;
|
|
383
391
|
if (!settings) {
|
|
@@ -442,6 +450,7 @@ export async function capturePages(config, options = {}) {
|
|
|
442
450
|
);
|
|
443
451
|
const timings = {};
|
|
444
452
|
const gaps = [];
|
|
453
|
+
const leaks = [];
|
|
445
454
|
const rows = [];
|
|
446
455
|
const files = new Map();
|
|
447
456
|
const fontUrls = new Map();
|
|
@@ -547,10 +556,7 @@ export async function capturePages(config, options = {}) {
|
|
|
547
556
|
].join("\n"),
|
|
548
557
|
needles,
|
|
549
558
|
);
|
|
550
|
-
if (found)
|
|
551
|
-
throw new Error(
|
|
552
|
-
"the page's markup carries a forbidStrings value; not written",
|
|
553
|
-
);
|
|
559
|
+
if (found) throw new ForbiddenStringError();
|
|
554
560
|
|
|
555
561
|
// CSS assets on the app origin (other than fonts) are inlined.
|
|
556
562
|
const inlined = new Map();
|
|
@@ -610,6 +616,11 @@ export async function capturePages(config, options = {}) {
|
|
|
610
616
|
`captured ${id} (${capture.layout}, slots ${capture.slots.join(",")}, ${Math.round(Buffer.byteLength(html) / 1024)} KB, ${((performance.now() - pageStart) / 1000).toFixed(1)}s)`,
|
|
611
617
|
);
|
|
612
618
|
} catch (error) {
|
|
619
|
+
if (error instanceof ForbiddenStringError) {
|
|
620
|
+
leaks.push(id);
|
|
621
|
+
log(`${id}: ${error.message}`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
613
624
|
const reason = String(error?.message ?? error)
|
|
614
625
|
.split("\n")
|
|
615
626
|
.slice(0, 3)
|
|
@@ -630,6 +641,12 @@ export async function capturePages(config, options = {}) {
|
|
|
630
641
|
fs.rmSync(shotsDir, { recursive: true, force: true });
|
|
631
642
|
}
|
|
632
643
|
|
|
644
|
+
// A leaked value fails the whole capture: no page of this run is written,
|
|
645
|
+
// so a CI job stops before it uploads anything.
|
|
646
|
+
if (leaks.length)
|
|
647
|
+
throw new Error(
|
|
648
|
+
`${leaks.join(", ")} carried a forbidStrings value; nothing was written`,
|
|
649
|
+
);
|
|
633
650
|
writePagesKit(out, {
|
|
634
651
|
rows,
|
|
635
652
|
files,
|
package/src/cli.mjs
CHANGED
|
@@ -11,8 +11,17 @@ const VERSION = JSON.parse(
|
|
|
11
11
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
12
12
|
).version;
|
|
13
13
|
|
|
14
|
+
/** The `design-kit-setup` skill, printed by `design-kit setup-guide`. */
|
|
15
|
+
const SETUP_GUIDE = new URL(
|
|
16
|
+
"../skills/design-kit-setup/SKILL.md",
|
|
17
|
+
import.meta.url,
|
|
18
|
+
);
|
|
19
|
+
|
|
14
20
|
const USAGE = `usage: design-kit <command> [options]
|
|
15
21
|
|
|
22
|
+
init set a kit up in this repository: the config, a daily
|
|
23
|
+
GitHub Actions build, and a skill for its agents
|
|
24
|
+
[--root <dir>] [--dry-run] [--force]
|
|
16
25
|
capture stories capture every story of a Storybook 7/8 static build
|
|
17
26
|
[--config <file>] [--out <dir>] [--storybook-dir <dir>]
|
|
18
27
|
[--only <id,...> | --only-file <file>] [--base <previous kit>]
|
|
@@ -24,10 +33,13 @@ const USAGE = `usage: design-kit <command> [options]
|
|
|
24
33
|
[--kit <dir>] default: DESIGN_KIT_DIR, the config's "out", ./design-kit
|
|
25
34
|
validate check design-kit.config.json against the v1 schema
|
|
26
35
|
[--config <file>]
|
|
36
|
+
setup-guide print the guide for setting a kit up in a repository as
|
|
37
|
+
one reviewable pull request (an agent skill)
|
|
27
38
|
|
|
28
39
|
The config is the nearest design-kit.config.json at or above the working
|
|
29
40
|
directory unless --config names one. Exit codes: 0 done, 1 a capture could
|
|
30
|
-
not run, 2 a config or usage
|
|
41
|
+
not run (or init found the repository already set up), 2 a config or usage
|
|
42
|
+
error.`;
|
|
31
43
|
|
|
32
44
|
function parse(argv, spec) {
|
|
33
45
|
const options = {};
|
|
@@ -153,6 +165,24 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
153
165
|
return 0;
|
|
154
166
|
}
|
|
155
167
|
if (command === "kit") return await kitCommand(argv.slice(1), env);
|
|
168
|
+
if (command === "init") {
|
|
169
|
+
const options = parse(argv.slice(1), {
|
|
170
|
+
root: "value",
|
|
171
|
+
"dry-run": "flag",
|
|
172
|
+
force: "flag",
|
|
173
|
+
});
|
|
174
|
+
const { init } = await import("./init.mjs");
|
|
175
|
+
return init({
|
|
176
|
+
root: options.root ?? ".",
|
|
177
|
+
dryRun: Boolean(options["dry-run"]),
|
|
178
|
+
force: Boolean(options.force),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (command === "setup-guide") {
|
|
182
|
+
parse(argv.slice(1), {});
|
|
183
|
+
process.stdout.write(readFileSync(SETUP_GUIDE, "utf8"));
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
156
186
|
if (command === "validate") {
|
|
157
187
|
const options = parse(argv.slice(1), { config: "value" });
|
|
158
188
|
const config = loadConfig(options.config);
|