@codepress/design-kit 0.1.0 → 0.1.2
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 +47 -3
- package/package.json +6 -3
- package/src/capture-pages.mjs +22 -5
- package/src/cli.mjs +18 -1
- package/src/init.mjs +1017 -0
- package/src/kit.d.ts +81 -0
- package/src/kit.mjs +44 -12
- package/src/templates/design-kit-skill.md +156 -0
package/src/kit.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Types for `@codepress/design-kit/kit`, the kit tool's functions. */
|
|
2
|
+
|
|
3
|
+
/** One row of `index.json` (stories) or `routes.json` (pages). */
|
|
4
|
+
export interface KitRow {
|
|
5
|
+
id: string;
|
|
6
|
+
kind?: "story" | "route";
|
|
7
|
+
title?: string;
|
|
8
|
+
component?: string;
|
|
9
|
+
file: string;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
bytes?: number;
|
|
13
|
+
slots?: string[];
|
|
14
|
+
source_file?: string;
|
|
15
|
+
body_class?: string;
|
|
16
|
+
html_attrs?: Record<string, string>;
|
|
17
|
+
body_attrs?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface KitManifest {
|
|
21
|
+
kit_version?: number;
|
|
22
|
+
sha?: string;
|
|
23
|
+
built_at?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A loaded kit: its folder, manifest and rows, sorted and by id. */
|
|
28
|
+
export interface Kit {
|
|
29
|
+
dir: string;
|
|
30
|
+
manifest: KitManifest | null;
|
|
31
|
+
rows: KitRow[];
|
|
32
|
+
byId: Map<string, KitRow>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadKit(dir: string): Kit;
|
|
36
|
+
|
|
37
|
+
/** Rows whose id, title, component or source file holds every word. */
|
|
38
|
+
export function find(kit: Kit, query: string): KitRow[];
|
|
39
|
+
|
|
40
|
+
/** Up to five candidates, closest first. */
|
|
41
|
+
export function nearMatches(wanted: string, candidates: string[]): string[];
|
|
42
|
+
|
|
43
|
+
/** A markup outline of at most `limit` bytes. */
|
|
44
|
+
export function outline(
|
|
45
|
+
html: string,
|
|
46
|
+
limit?: number,
|
|
47
|
+
): { lines: string[]; truncated: boolean };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One self-contained page. Throws an Error whose message starts with
|
|
51
|
+
* `unknown ` (and names near matches) for an unknown frame, slot or snippet,
|
|
52
|
+
* and with `too large: ` when snippets grow the page past `maxBytes`.
|
|
53
|
+
*/
|
|
54
|
+
export function compose(
|
|
55
|
+
kit: Kit,
|
|
56
|
+
options: {
|
|
57
|
+
frame: string;
|
|
58
|
+
slots?: { name: string; content: string }[];
|
|
59
|
+
theme?: "light" | "dark";
|
|
60
|
+
showNew?: boolean;
|
|
61
|
+
/** Bytes of markup the page may grow to through snippets. */
|
|
62
|
+
maxBytes?: number;
|
|
63
|
+
},
|
|
64
|
+
): string;
|
|
65
|
+
|
|
66
|
+
/** The default `maxBytes` of `compose`: 32 MB. */
|
|
67
|
+
export const COMPOSE_MAX_BYTES: number;
|
|
68
|
+
|
|
69
|
+
export function sanitizeMarkup(html: string): string;
|
|
70
|
+
export function sanitizeCss(css: string): string;
|
|
71
|
+
|
|
72
|
+
/** Runs one command line; returns the exit code. */
|
|
73
|
+
export function run(
|
|
74
|
+
argv: string[],
|
|
75
|
+
io?: { out(text: string): void; err(text: string): void },
|
|
76
|
+
env?: Record<string, string | undefined>,
|
|
77
|
+
defaultDir?: string,
|
|
78
|
+
): number;
|
|
79
|
+
|
|
80
|
+
/** Whether this file is the script node was started with. */
|
|
81
|
+
export function isMain(moduleUrl?: string, script?: string): boolean;
|
package/src/kit.mjs
CHANGED
|
@@ -19,6 +19,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
19
19
|
const FIND_LIMIT = 30;
|
|
20
20
|
const SHOW_LIMIT = 2048;
|
|
21
21
|
const SNIPPET_DEPTH = 8;
|
|
22
|
+
/**
|
|
23
|
+
* Bytes of markup one compose may read through <kit-snippet>s. Nesting is
|
|
24
|
+
* capped, but a file of many placeholders pointing at large rows would
|
|
25
|
+
* otherwise grow the page exponentially.
|
|
26
|
+
*/
|
|
27
|
+
export const COMPOSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
22
28
|
|
|
23
29
|
class KitError extends Error {}
|
|
24
30
|
|
|
@@ -672,7 +678,7 @@ export function inlineAssets(kit, css, cssDir = join(kit.dir, "css")) {
|
|
|
672
678
|
|
|
673
679
|
// ── compose ─────────────────────────────────────────────────────────────────
|
|
674
680
|
|
|
675
|
-
function expandSnippets(kit, html, used, depth = 0) {
|
|
681
|
+
function expandSnippets(kit, html, used, budget, depth = 0) {
|
|
676
682
|
const found = elements(html).filter((e) => e.name === "kit-snippet");
|
|
677
683
|
if (!found.length) return html;
|
|
678
684
|
if (depth >= SNIPPET_DEPTH) {
|
|
@@ -689,10 +695,16 @@ function expandSnippets(kit, html, used, depth = 0) {
|
|
|
689
695
|
const row = requireRow(kit, id, "<kit-snippet id>");
|
|
690
696
|
used.add(rowSource(row));
|
|
691
697
|
let markup = readSnippet(kit, row);
|
|
698
|
+
budget.left -= markup.length;
|
|
699
|
+
if (budget.left < 0) {
|
|
700
|
+
throw new KitError(
|
|
701
|
+
`too large: the page grows past ${budget.max} bytes while expanding <kit-snippet> elements; use fewer or smaller snippets.`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
692
704
|
if (rowSource(row) === "stories") markup = unwrapStoryRoot(markup);
|
|
693
705
|
out +=
|
|
694
706
|
html.slice(i, element.start) +
|
|
695
|
-
expandSnippets(kit, markup, used, depth + 1);
|
|
707
|
+
expandSnippets(kit, markup, used, budget, depth + 1);
|
|
696
708
|
i = element.end;
|
|
697
709
|
}
|
|
698
710
|
return out + html.slice(i);
|
|
@@ -815,7 +827,13 @@ function attributes(captured, extraClass, theme, choice, target, fixed) {
|
|
|
815
827
|
*/
|
|
816
828
|
export function compose(
|
|
817
829
|
kit,
|
|
818
|
-
{
|
|
830
|
+
{
|
|
831
|
+
frame,
|
|
832
|
+
slots = [],
|
|
833
|
+
theme = "light",
|
|
834
|
+
showNew = false,
|
|
835
|
+
maxBytes = COMPOSE_MAX_BYTES,
|
|
836
|
+
},
|
|
819
837
|
) {
|
|
820
838
|
const row = requireRow(kit, frame, "frame");
|
|
821
839
|
if (theme !== "light" && theme !== "dark") {
|
|
@@ -823,7 +841,10 @@ export function compose(
|
|
|
823
841
|
}
|
|
824
842
|
const used = new Set([rowSource(row)]);
|
|
825
843
|
const filled = fillSlots(kit, row, readSnippet(kit, row), slots);
|
|
826
|
-
const body = expandSnippets(kit, filled, used
|
|
844
|
+
const body = expandSnippets(kit, filled, used, {
|
|
845
|
+
left: maxBytes - filled.length,
|
|
846
|
+
max: maxBytes,
|
|
847
|
+
});
|
|
827
848
|
|
|
828
849
|
const css = ["stories", "routes"]
|
|
829
850
|
.filter((source) => used.has(source))
|
|
@@ -1069,16 +1090,27 @@ export function run(
|
|
|
1069
1090
|
}
|
|
1070
1091
|
}
|
|
1071
1092
|
|
|
1072
|
-
/**
|
|
1073
|
-
|
|
1074
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* Whether this file is the script node was started with (symlinks resolved).
|
|
1095
|
+
* Only a file named `kit.mjs` can be: a bundler that inlines this module into
|
|
1096
|
+
* another program keeps `import.meta.url` pointing at that program, which is
|
|
1097
|
+
* also the script node started, and the program would then run the kit's
|
|
1098
|
+
* command line on its own arguments.
|
|
1099
|
+
*/
|
|
1100
|
+
export function isMain(moduleUrl = import.meta.url, script = process.argv[1]) {
|
|
1101
|
+
if (!script) return false;
|
|
1102
|
+
let self;
|
|
1075
1103
|
try {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1104
|
+
self = fileURLToPath(moduleUrl);
|
|
1105
|
+
} catch {
|
|
1106
|
+
// Not a file URL: an evaluated bundle, never the script node started.
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
if (basename(self) !== "kit.mjs") return false;
|
|
1110
|
+
try {
|
|
1111
|
+
return realpathSync(self) === realpathSync(resolve(script));
|
|
1080
1112
|
} catch {
|
|
1081
|
-
return
|
|
1113
|
+
return moduleUrl === pathToFileURL(resolve(script)).href;
|
|
1082
1114
|
}
|
|
1083
1115
|
}
|
|
1084
1116
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-kit
|
|
3
|
+
description: "Draw mockups of this app's screens from its design kit: real pages and Storybook stories captured every day as static HTML, composed into self-contained pages with only the new parts hand-drawn. Use for any mockup, design or screen page of this app. Covers fetching the newest kit with gh, kit find/show/compose, <kit-snippet> and data-kit-new, staleness, captions saying what is real and what is new, and what to do when no kit can be fetched."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design kit
|
|
7
|
+
|
|
8
|
+
Mockups of this app's screens start from its **design kit**: real pages of
|
|
9
|
+
the app and its Storybook stories, rendered to static HTML with the app's own
|
|
10
|
+
CSS and fonts, plus `kit.mjs`, a dependency-free tool that finds pieces and
|
|
11
|
+
composes self-contained pages. Draw only what is new; the frame and every
|
|
12
|
+
existing component come from the kit, so the mockup looks like the app it will
|
|
13
|
+
ship in.
|
|
14
|
+
|
|
15
|
+
The kit is configured in `{{CONFIG}}` and built by the `Design kit` workflow
|
|
16
|
+
(`.github/workflows/design-kit.yml`). The format and every command are in the
|
|
17
|
+
`{{PACKAGE}}` README.
|
|
18
|
+
|
|
19
|
+
## 1. Fetch the newest kit
|
|
20
|
+
|
|
21
|
+
The workflow runs once a day and on demand (`workflow_dispatch`), and uploads
|
|
22
|
+
the `design-kit` artifact (kept 30 days). A day with no new commits skips and
|
|
23
|
+
uploads nothing, yet still succeeds, so look up the newest artifact rather than
|
|
24
|
+
the newest run, then download it once per kit commit:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
|
|
28
|
+
BRANCH=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
|
|
29
|
+
read -r RUN SHA < <(gh api "repos/$REPO/actions/artifacts?name=design-kit&per_page=100" \
|
|
30
|
+
--jq "[.artifacts[] | select((.expired | not) and .workflow_run.head_branch == \"$BRANCH\")]
|
|
31
|
+
| sort_by(.created_at) | last // empty | \"\(.workflow_run.id) \(.workflow_run.head_sha)\"")
|
|
32
|
+
echo "run=$RUN sha=$SHA" # empty → go to "When there is no kit"
|
|
33
|
+
KIT="${TMPDIR:-/tmp}/design-kit-$SHA"
|
|
34
|
+
if [ -n "$RUN" ] && [ ! -f "$KIT/manifest.json" ]; then
|
|
35
|
+
rm -rf "$KIT.partial" &&
|
|
36
|
+
gh run download "$RUN" --repo "$REPO" -n design-kit -D "$KIT.partial" &&
|
|
37
|
+
mv "$KIT.partial" "$KIT"
|
|
38
|
+
fi
|
|
39
|
+
node "$KIT/kit.mjs" info # manifest: sha, built_at, counts, sources
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Reuse `$KIT` while the newest kit's SHA is unchanged. Keep the kit and your
|
|
43
|
+
working files in a scratch folder, never in the repository.
|
|
44
|
+
|
|
45
|
+
## 2. Check staleness
|
|
46
|
+
|
|
47
|
+
The kit is a snapshot of `manifest.sha`, up to a day behind. Check whether UI
|
|
48
|
+
code changed since:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
KIT_SHA=$(node -p 'require(process.argv[1]).sha' "$KIT/manifest.json")
|
|
52
|
+
git fetch -q origin "$BRANCH" && git log --oneline "$KIT_SHA..origin/$BRANCH" -- {{WATCHED_PATHS}}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When the list is not empty, say so on the mockup: "Kit from `<sha7>`; N UI
|
|
56
|
+
commits since, so parts of the frame may be out of date". When one of those
|
|
57
|
+
commits touches a component the mockup shows, read it and say what the mockup
|
|
58
|
+
is missing.
|
|
59
|
+
|
|
60
|
+
## 3. Find what is real
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node "$KIT/kit.mjs" find button # ≤ 30 lines: id, kind, width, bytes, title
|
|
64
|
+
node "$KIT/kit.mjs" show route:home@1280 # path, size and an outline with slots
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Frames** are whole screens: `route:<page>@<width>` rows, real pages of the
|
|
68
|
+
app at 1280 and 390 wide, with replaceable regions marked `data-kit-slot`
|
|
69
|
+
(`main` on every page). New content goes only into a slot, and only `route:`
|
|
70
|
+
frames have slots. A story frame is still useful unchanged, as the "today"
|
|
71
|
+
view or as real context beside a mockup.
|
|
72
|
+
- **Snippets** are stories: `story:<storybook-id>@<width>`, one per component
|
|
73
|
+
state. Search by component and state (`find empty`, `find dialog`).
|
|
74
|
+
- `show` prints a short outline. Use `show --full` only on a small snippet
|
|
75
|
+
whose structure you need, never on a frame.
|
|
76
|
+
- Anything not in the kit has no story or its capture failed (`gaps.json` in
|
|
77
|
+
the kit lists the failures). Draw it as new and say it is approximated.
|
|
78
|
+
|
|
79
|
+
## 4. Write only the new content
|
|
80
|
+
|
|
81
|
+
Write one HTML fragment per slot you change, in your scratch folder:
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<!-- $WORK/after-main.html → fills the frame's `main` slot -->
|
|
85
|
+
<div class="page">
|
|
86
|
+
<kit-snippet id="story:components-card--default@1280"></kit-snippet>
|
|
87
|
+
<section data-kit-new class="card">…only what the change adds…</section>
|
|
88
|
+
</div>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `<kit-snippet id="…">` places a real component; `compose` expands it with
|
|
92
|
+
the captured markup.
|
|
93
|
+
- Mark every hand-drawn element with `data-kit-new` (the outermost new element
|
|
94
|
+
is enough).
|
|
95
|
+
- Style new parts with the app's own classes and CSS variables. The kit's CSS
|
|
96
|
+
holds only the rules the app actually loads, so check a class exists with
|
|
97
|
+
`grep -c '\.<class>' "$KIT"/css/*.css`, or use an inline `style` with the
|
|
98
|
+
app's variables.
|
|
99
|
+
- To show several states of the new part, stack them inside the fragment, each
|
|
100
|
+
with a short label.
|
|
101
|
+
|
|
102
|
+
## 5. Compose
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node "$KIT/kit.mjs" compose --frame route:home@1280 \
|
|
106
|
+
--slot main="$WORK/after-main.html" --out "$WORK/after-desktop.html"
|
|
107
|
+
node "$KIT/kit.mjs" compose --frame route:home@1280 --out "$WORK/today.html"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The result is one self-contained page: CSS and fonts inlined, scripts and
|
|
111
|
+
remote URLs removed, and `data-kit-sha` and `data-kit-id` on `<html>`. Add
|
|
112
|
+
`--theme dark` for the dark theme and `--show-new` for a dashed outline on
|
|
113
|
+
every `data-kit-new` element. An unknown frame, slot or snippet id exits 2 and
|
|
114
|
+
names near matches.
|
|
115
|
+
|
|
116
|
+
Never paste snippet or frame markup through your own output, and do not read a
|
|
117
|
+
composed page back: it carries the whole CSS and fonts. Open it in a browser,
|
|
118
|
+
screenshot it, and share the screenshot or the file.
|
|
119
|
+
|
|
120
|
+
## 6. Caption: what is real, what is new
|
|
121
|
+
|
|
122
|
+
Under every mockup, say which parts are real and which are new, and where you
|
|
123
|
+
approximated:
|
|
124
|
+
|
|
125
|
+
> Real: the Home page (`route:home@1280`) and the card
|
|
126
|
+
> (`story:components-card--default@1280`), from kit `3f2a9c1` built
|
|
127
|
+
> 2026-11-03. New (dashed): the Pinned section. Approximated: the pin menu has
|
|
128
|
+
> no story yet.
|
|
129
|
+
|
|
130
|
+
Add the staleness line from step 2 when it applies. For other widths, compose
|
|
131
|
+
the capture on the same side of the breakpoint and say that layout decided in
|
|
132
|
+
script stays as captured.
|
|
133
|
+
|
|
134
|
+
## When there is no kit
|
|
135
|
+
|
|
136
|
+
1. **Build it locally** from an up-to-date checkout with dependencies
|
|
137
|
+
installed (`{{INSTALL}}`):
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
DK="{{CLI}}"
|
|
141
|
+
CONFIG="$PWD/{{CONFIG}}"
|
|
142
|
+
KIT="${TMPDIR:-/tmp}/design-kit-local-$(git rev-parse --short HEAD)"
|
|
143
|
+
{{PLAYWRIGHT_INSTALL}}
|
|
144
|
+
{{STORIES_LINE}}
|
|
145
|
+
$DK capture pages --config "$CONFIG" --out "$KIT" # starts the app itself
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then follow steps 3–6 with this `$KIT` and caption it as "local build at
|
|
149
|
+
`<sha7>`". A page capture that fails leaves a kit of stories only: compose
|
|
150
|
+
story frames unchanged for real context, draw the new screen by hand and
|
|
151
|
+
say so.
|
|
152
|
+
|
|
153
|
+
2. **No kit at all**: approximate from the app's styles and components, and say
|
|
154
|
+
on every mockup "Approximated: the design kit was unavailable (<reason>)".
|
|
155
|
+
|
|
156
|
+
Either way the work goes ahead; the caption says what you could not use.
|