@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 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. A page whose output contains one, as written, escaped for
113
- HTML or JSON, or (for a token) any long segment of it, is not written.
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), 2 a config or usage error. A story or page that fails is a
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.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.
@@ -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 error.`;
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);