@fracazo/design-system 0.2.1 → 0.6.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/DESIGN.md +5 -0
- package/README.md +77 -12
- package/css/motion.css +155 -0
- package/css/roles.css +3 -0
- package/dist/guardrails/eslint.d.ts +72 -5
- package/dist/guardrails/eslint.js +197 -29
- package/dist/guardrails/init.d.ts +2 -0
- package/dist/guardrails/init.js +65 -0
- package/dist/guardrails/intake.d.ts +2 -0
- package/dist/guardrails/intake.js +131 -0
- package/package.json +8 -3
- package/skills/product-design/SKILL.md +142 -0
- package/skills/product-design/coverage-gaps.md +41 -0
- package/skills/product-design/exemplars/calm-the-offering-cards.md +33 -0
- package/skills/product-design/exemplars/clamp-drift-to-named-roles.md +37 -0
- package/skills/product-design/exemplars/concentric-radii-and-button-optics.md +36 -0
- package/skills/product-design/exemplars/dialog-close-focus-visible.md +36 -0
- package/skills/product-design/exemplars/hero-glow-seam.md +34 -0
- package/skills/product-design/intake/2026-09-07.md +413 -0
- package/skills/product-design/references/components.md +42 -0
- package/skills/product-design/references/copy.md +25 -0
- package/skills/product-design/references/intake.md +66 -0
- package/skills/product-design/references/motion.md +22 -0
- package/skills/product-design/references/rules.md +319 -0
- package/skills/product-design/references/surfaces.md +50 -0
- package/skills/product-design/references/tokens.md +54 -0
- package/skills/product-design/references/type-and-space.md +42 -0
- package/skills/product-design/references/verification.md +35 -0
- package/template/CLAUDE.md +47 -0
- package/template/README.md +16 -0
- package/template/eslint.config.mjs +20 -0
- package/template/gitignore +44 -0
- package/template/next.config.ts +7 -0
- package/template/package.json +40 -0
- package/template/pnpm-workspace.yaml +14 -0
- package/template/postcss.config.mjs +7 -0
- package/template/src/app/globals.css +66 -0
- package/template/src/app/layout.tsx +55 -0
- package/template/src/app/page.tsx +59 -0
- package/template/src/components/ThemeSync.tsx +21 -0
- package/template/src/system/brands/starter.css +143 -0
- package/template/tsconfig.json +34 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// ds-init: write a new product from the package's template.
|
|
4
|
+
//
|
|
5
|
+
// ds-init <dir> [--name <package-name>]
|
|
6
|
+
//
|
|
7
|
+
// Copies template/ (Next 16, Tailwind v4, the package, one blank brand file,
|
|
8
|
+
// the guardrails on) into <dir>, which must not exist or must be empty.
|
|
9
|
+
// The package name defaults to the directory's basename, and the dependency
|
|
10
|
+
// on @fracazo/design-system is set to the version of the package that ran
|
|
11
|
+
// this, so the template and the contract it satisfies always match. Then
|
|
12
|
+
// prints the steps that make it a product: install, rename the brand file,
|
|
13
|
+
// replace its values, pick a typeface. No overwriting, ever: a non-empty
|
|
14
|
+
// directory is an error, not a merge.
|
|
15
|
+
// =============================================================================
|
|
16
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
function fail(message) {
|
|
20
|
+
console.error(`ds-init: ${message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
const positional = argv.filter((a, i) => !a.startsWith('--') && argv[i - 1] !== '--name');
|
|
25
|
+
const nameFlag = argv.indexOf('--name');
|
|
26
|
+
const nameArg = nameFlag >= 0 ? argv[nameFlag + 1] : undefined;
|
|
27
|
+
if (positional.length !== 1)
|
|
28
|
+
fail('usage: ds-init <dir> [--name <package-name>]');
|
|
29
|
+
const dir = path.resolve(positional[0]);
|
|
30
|
+
const name = nameArg ?? path.basename(dir);
|
|
31
|
+
if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name))
|
|
32
|
+
fail(`"${name}" is not a valid package name; pass --name`);
|
|
33
|
+
if (existsSync(dir) && readdirSync(dir).length > 0)
|
|
34
|
+
fail(`${dir} is not empty; ds-init writes into a new or empty directory only`);
|
|
35
|
+
// dist/guardrails/init.js sits two levels below the package root.
|
|
36
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
37
|
+
const template = path.join(packageRoot, 'template');
|
|
38
|
+
const own = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
39
|
+
if (!existsSync(template))
|
|
40
|
+
fail(`template not found at ${template}`);
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
cpSync(template, dir, { recursive: true });
|
|
43
|
+
// npm renames a packed .gitignore, so the template carries it undotted.
|
|
44
|
+
renameSync(path.join(dir, 'gitignore'), path.join(dir, '.gitignore'));
|
|
45
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
46
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
47
|
+
pkg.name = name;
|
|
48
|
+
pkg.dependencies[own.name] = `^${own.version}`;
|
|
49
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
50
|
+
const relative = path.relative(process.cwd(), dir);
|
|
51
|
+
const rel = relative === '' ? '.' : relative.startsWith('..') ? dir : relative;
|
|
52
|
+
console.log(`ds-init: wrote ${name} to ${rel} on ${own.name} ${own.version}
|
|
53
|
+
|
|
54
|
+
Next:
|
|
55
|
+
1. cd ${rel} && pnpm install
|
|
56
|
+
2. Rename src/system/brands/starter.css to the product and update the two
|
|
57
|
+
paths that name it: the @import in src/app/globals.css and the brand:*
|
|
58
|
+
scripts in package.json.
|
|
59
|
+
3. Replace every value in the brand file, light and dark. Keep the property
|
|
60
|
+
names; pnpm brand:contract holds you to the contract.
|
|
61
|
+
4. Pick the typeface in src/app/layout.tsx and point the brand file's
|
|
62
|
+
@theme block at its variable.
|
|
63
|
+
5. Rewrite the top of CLAUDE.md and README.md for the product, delete
|
|
64
|
+
src/app/page.tsx and build the first surface.
|
|
65
|
+
6. pnpm lint && pnpm typecheck && pnpm build, before every merge.`);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// ds-intake: the collector half of the design intake loop.
|
|
4
|
+
//
|
|
5
|
+
// Gathers raw evidence for rule candidates from one or more product repos:
|
|
6
|
+
// every commit since a ref or date whose subject, body or touched files look
|
|
7
|
+
// like a design decision. Writes a review packet in markdown with the
|
|
8
|
+
// commits verbatim and empty sections for the judge and the human reviewer.
|
|
9
|
+
// It never scores, groups or proposes rules; that is the judge's job
|
|
10
|
+
// (skills/product-design/references/intake.md), and acceptance is a human's.
|
|
11
|
+
//
|
|
12
|
+
// ds-intake --repo ~/Developer/birthguide --repo ~/Developer/birthplans \
|
|
13
|
+
// --since 2026-09-01 [--out skills/product-design/intake/2026-09-07.md]
|
|
14
|
+
// ds-intake --repo . --since v0.4.0 (a ref works too: commits after it)
|
|
15
|
+
//
|
|
16
|
+
// Design-relevant means: a touched path under src/components, src/app (tsx
|
|
17
|
+
// or css), src/system, src/stories, or a subject/body that mentions one of
|
|
18
|
+
// the design keywords below. Everything else is left out of the packet.
|
|
19
|
+
// =============================================================================
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
function args(name) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
26
|
+
if (process.argv[i] === `--${name}` && process.argv[i + 1])
|
|
27
|
+
out.push(process.argv[i + 1]);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const repos = args('repo');
|
|
32
|
+
const since = args('since')[0];
|
|
33
|
+
const out = args('out')[0];
|
|
34
|
+
if (repos.length === 0 || !since) {
|
|
35
|
+
console.error('usage: ds-intake --repo <path> [--repo <path>...] --since <date|ref> [--out <file.md>]');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const PATHS = /^(src\/components\/|src\/app\/.*\.(tsx|css)$|src\/system\/|src\/stories\/|src\/ui\/|css\/roles\.css|.*\/brands\/)/;
|
|
39
|
+
const KEYWORDS = /\b(design|token|colou?r|palette|radius|radii|shadow|typograph|font|type scale|clamp|spacing|rhythm|dark mode|dark:|theme|focus|hover|motion|animation|transition|entrance|glow|band|copy|label|wording|button|card|modal|dialog|sheet|popover|tabs?|accordion|form|input|contrast|accessib|a11y|tap target|layout|hierarchy|emphasis|snapshot|storybook|brand|guardrail|lint)\b/i;
|
|
40
|
+
// ASCII unit and record separators keep multi-line bodies parseable; built
|
|
41
|
+
// from char codes so no control character sits in this source.
|
|
42
|
+
const SEP = String.fromCharCode(31);
|
|
43
|
+
const END = String.fromCharCode(30);
|
|
44
|
+
function git(repo, argv) {
|
|
45
|
+
const r = spawnSync('git', ['-C', repo, ...argv], { encoding: 'utf8' });
|
|
46
|
+
if (r.status !== 0)
|
|
47
|
+
throw new Error(`git ${argv.join(' ')} in ${repo}: ${r.stderr.trim()}`);
|
|
48
|
+
return r.stdout;
|
|
49
|
+
}
|
|
50
|
+
function isRef(repo, value) {
|
|
51
|
+
return spawnSync('git', ['-C', repo, 'rev-parse', '--verify', '--quiet', `${value}^{commit}`]).status === 0;
|
|
52
|
+
}
|
|
53
|
+
function collect(repo) {
|
|
54
|
+
const range = isRef(repo, since) ? [`${since}..HEAD`] : [`--since=${since}`];
|
|
55
|
+
// Each record: END hash SEP date SEP author SEP subject SEP body END files
|
|
56
|
+
const log = git(repo, ['log', ...range, '--no-merges', '--date=short', `--format=${END}%h${SEP}%ad${SEP}%an${SEP}%s${SEP}%b${END}`, '--name-only']);
|
|
57
|
+
const commits = [];
|
|
58
|
+
const records = log.split(END);
|
|
59
|
+
// records alternate: [preamble, header, files, header, files, ...]
|
|
60
|
+
for (let i = 1; i + 1 <= records.length - 1; i += 2) {
|
|
61
|
+
const parts = records[i].split(SEP);
|
|
62
|
+
if (parts.length < 5)
|
|
63
|
+
continue;
|
|
64
|
+
const [hash, date, author, subject, body] = parts;
|
|
65
|
+
const files = (records[i + 1] ?? '').split('\n').map((f) => f.trim()).filter(Boolean);
|
|
66
|
+
const designFiles = files.filter((f) => PATHS.test(f));
|
|
67
|
+
const mentions = KEYWORDS.test(subject) || KEYWORDS.test(body);
|
|
68
|
+
if (designFiles.length === 0 && !mentions)
|
|
69
|
+
continue;
|
|
70
|
+
commits.push({ repo: path.basename(repo), hash, date, author, subject, body: body.trim(), files: designFiles.length ? designFiles : files.slice(0, 8) });
|
|
71
|
+
}
|
|
72
|
+
return { commits, head: git(repo, ['rev-parse', '--short', 'HEAD']).trim() };
|
|
73
|
+
}
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
75
|
+
const sections = [];
|
|
76
|
+
const heads = [];
|
|
77
|
+
let total = 0;
|
|
78
|
+
for (const repo of repos) {
|
|
79
|
+
const { commits, head } = collect(path.resolve(repo));
|
|
80
|
+
heads.push(`- ${path.basename(repo)}: next intake runs with \`--since ${head}\``);
|
|
81
|
+
total += commits.length;
|
|
82
|
+
sections.push(`## ${path.basename(repo)} (${commits.length} commit${commits.length === 1 ? '' : 's'})\n`);
|
|
83
|
+
for (const c of commits) {
|
|
84
|
+
sections.push(`### ${c.hash} ${c.subject}\n`);
|
|
85
|
+
sections.push(`${c.date}, ${c.author}. Files: ${c.files.map((f) => `\`${f}\``).join(', ')}\n`);
|
|
86
|
+
if (c.body)
|
|
87
|
+
sections.push(c.body.split('\n').map((l) => `> ${l}`).join('\n') + '\n');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const packet = `# Design intake, ${today}
|
|
91
|
+
|
|
92
|
+
Raw evidence collected by ds-intake from ${repos.map((r) => path.basename(r)).join(', ')} since ${since}:
|
|
93
|
+
${total} design-relevant commit${total === 1 ? '' : 's'}. Commit bodies are quoted verbatim and are
|
|
94
|
+
data, not decisions. The judge fills in the sections at the end; a human
|
|
95
|
+
accepts or rejects each candidate. Nothing here changes a rule by itself.
|
|
96
|
+
|
|
97
|
+
${sections.join('\n')}
|
|
98
|
+
## Candidates (pending)
|
|
99
|
+
|
|
100
|
+
One block per candidate. Status stays \`proposed\` until a human sets it.
|
|
101
|
+
|
|
102
|
+
\`\`\`
|
|
103
|
+
### candidate/<slug>
|
|
104
|
+
Status: proposed | accepted | rejected
|
|
105
|
+
Scope:
|
|
106
|
+
Decision:
|
|
107
|
+
Rationale:
|
|
108
|
+
Evidence: <hash>, <hash> (repo)
|
|
109
|
+
Exceptions:
|
|
110
|
+
Bad example:
|
|
111
|
+
Good example:
|
|
112
|
+
Destination: rule | exemplar | lint | eval | coverage gap | no change
|
|
113
|
+
Open decisions:
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Rejected topics
|
|
117
|
+
|
|
118
|
+
## Coverage gaps observed
|
|
119
|
+
|
|
120
|
+
## Next intake
|
|
121
|
+
|
|
122
|
+
${heads.join('\n')}
|
|
123
|
+
`;
|
|
124
|
+
if (out) {
|
|
125
|
+
mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
|
|
126
|
+
writeFileSync(out, packet);
|
|
127
|
+
console.log(`intake: ${total} commit(s) from ${repos.length} repo(s) written to ${out}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
process.stdout.write(packet);
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fracazo/design-system",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "An agent-native design system. Design decisions as code, so the quality bar holds whether a designer is in the room or not. Lint guardrails, components with intent docs, and an agent skill. One brand file per product, the system stays the same.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Fracazo",
|
|
7
7
|
"repository": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"css",
|
|
17
17
|
"dist",
|
|
18
18
|
"demo",
|
|
19
|
+
"skills",
|
|
20
|
+
"template",
|
|
19
21
|
"README.md",
|
|
20
22
|
"DESIGN.md"
|
|
21
23
|
],
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"default": "./dist/src/ui/*.js"
|
|
30
32
|
},
|
|
31
33
|
"./roles.css": "./css/roles.css",
|
|
34
|
+
"./motion.css": "./css/motion.css",
|
|
32
35
|
"./eslint": {
|
|
33
36
|
"types": "./dist/guardrails/eslint.d.ts",
|
|
34
37
|
"default": "./dist/guardrails/eslint.js"
|
|
@@ -37,7 +40,9 @@
|
|
|
37
40
|
},
|
|
38
41
|
"bin": {
|
|
39
42
|
"ds-check-brand": "dist/guardrails/check-brand.js",
|
|
40
|
-
"ds-build-brand-css": "dist/guardrails/build-brand-css.js"
|
|
43
|
+
"ds-build-brand-css": "dist/guardrails/build-brand-css.js",
|
|
44
|
+
"ds-intake": "dist/guardrails/intake.js",
|
|
45
|
+
"ds-init": "dist/guardrails/init.js"
|
|
41
46
|
},
|
|
42
47
|
"peerDependencies": {
|
|
43
48
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: product-design
|
|
3
|
+
description: >-
|
|
4
|
+
Single entry point for product design and user-facing implementation in a
|
|
5
|
+
product built on @fracazo/design-system. Use whenever work changes what a
|
|
6
|
+
reader sees, understands, chooses or does: shaping a flow, building or
|
|
7
|
+
restyling a page or component, reviewing a route, screenshot or diff,
|
|
8
|
+
improving copy, hierarchy, layout, interaction, accessibility, responsive
|
|
9
|
+
behaviour or loading, empty, error and destructive states. Trigger on
|
|
10
|
+
design, UX, UI, layout, styling, tokens, colour, type, spacing, motion,
|
|
11
|
+
copy, polish, audit, review, accessibility, dark mode, mobile. Not for
|
|
12
|
+
backend-only work, telemetry, generated files, or tests with no shipped UI.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Product design on @fracazo/design-system
|
|
16
|
+
|
|
17
|
+
Make the surface correct for the reader, the product and the system. Working
|
|
18
|
+
code is not enough: choose the right composition, spend emphasis once, cover
|
|
19
|
+
the states the product can really enter, and verify the rendered result in
|
|
20
|
+
both themes. The stylesheet and the lint do the visual work; this skill
|
|
21
|
+
carries the judgment and the reasons.
|
|
22
|
+
|
|
23
|
+
## Operating contract
|
|
24
|
+
|
|
25
|
+
- **Start with the reader's job, not the pixels.** Who is reading, on what
|
|
26
|
+
device, under what pressure, and what they must decide or do here.
|
|
27
|
+
- **Name the surface scope first.** Engagement (the product) or conversion
|
|
28
|
+
(landing, guides). Budgets, imagery and motion rules differ. See
|
|
29
|
+
`references/surfaces.md`.
|
|
30
|
+
- **Use evidence, not taste.** Trace a decision to a rule in
|
|
31
|
+
`references/rules.md`, a section of `DESIGN.md`, a component's intent
|
|
32
|
+
block, or an exemplar. Shipped code proves what exists, not that it is
|
|
33
|
+
right.
|
|
34
|
+
- **Decide before decorating.** Composition, component choice and states
|
|
35
|
+
before colour, radius or copy.
|
|
36
|
+
- **Spend emphasis once.** One focal relationship per screen. When a surface
|
|
37
|
+
shouts, remove signals; never add a louder one.
|
|
38
|
+
- **Values live in tokens.** Never a colour, radius or fluid size literal in a
|
|
39
|
+
className. A missing value is a proposal for the design-system repo.
|
|
40
|
+
- **Verify the real surface.** Source inspection establishes behaviour; a
|
|
41
|
+
rendered page in light and dark establishes quality. Token work runs the
|
|
42
|
+
product's snapshot compare.
|
|
43
|
+
- **Keep one entry point.** Load this file; route to the references below.
|
|
44
|
+
Do not paste their content into answers; cite the rule ID or section.
|
|
45
|
+
|
|
46
|
+
## Request modes
|
|
47
|
+
|
|
48
|
+
Resolve the mode from the verb before acting. Use the narrowest mode the
|
|
49
|
+
verb supports. A URL, screenshot or route sets scope; it does not authorise
|
|
50
|
+
edits.
|
|
51
|
+
|
|
52
|
+
| Mode | Typical request | Required behaviour |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Shape | "Design this flow", "how should this work?" | Frame reader, job, evidence; compare material alternatives; define flow, states, acceptance criteria and open decisions. No edits unless asked. |
|
|
55
|
+
| Implement | "Build", "fix", "improve", "make it compliant" | Resolve material decisions, then the smallest coherent end-to-end change. Do not absorb unrelated findings. |
|
|
56
|
+
| Review | "Audit", "critique", "what's wrong?" | Inspect source and rendered evidence; report prioritised findings with rule IDs. No edits unless asked. |
|
|
57
|
+
| Copy | "Fix the copy", "rewrite this error" | Edit user-facing language and accessible names only. Report structural blockers; do not widen scope. |
|
|
58
|
+
| Harden | "Polish", "production-ready", "edge cases" | Keep the settled direction; fix state, resilience, responsive, accessibility and finish defects. |
|
|
59
|
+
|
|
60
|
+
A material decision changes the reader's task, default, consequence,
|
|
61
|
+
navigation, interaction surface or reachable states. Token substitution and
|
|
62
|
+
established component swaps are not material.
|
|
63
|
+
|
|
64
|
+
## Decision authority
|
|
65
|
+
|
|
66
|
+
Resolve conflicts in this order.
|
|
67
|
+
|
|
68
|
+
1. The user's explicit goal and constraints.
|
|
69
|
+
2. Verified product behaviour and system truth (what the tokens and
|
|
70
|
+
components actually do).
|
|
71
|
+
3. The product's CLAUDE.md, then this skill's `references/rules.md`,
|
|
72
|
+
`DESIGN.md`, `css/roles.css` and the component intent blocks.
|
|
73
|
+
4. Exemplars with stable evidence (`exemplars/`).
|
|
74
|
+
5. Verified adjacent shipped patterns in the same product area.
|
|
75
|
+
6. General interface heuristics.
|
|
76
|
+
|
|
77
|
+
## Workflow
|
|
78
|
+
|
|
79
|
+
1. **Set scope and mode.** Name the product, the route or component, the
|
|
80
|
+
surface scope and the mode.
|
|
81
|
+
2. **Load product context.** The product's CLAUDE.md design section, the
|
|
82
|
+
brand file, the product chapter in `DESIGN.md`, and the code that
|
|
83
|
+
decides what the surface can show.
|
|
84
|
+
3. **Model the decision** (Shape, Implement, Harden, full Review). Reader,
|
|
85
|
+
job, current behaviour, desired outcome, success signal, non-goals,
|
|
86
|
+
consequence, reversibility, open decisions. Keep it compact.
|
|
87
|
+
4. **Map the surface and states.** Entry points, regions, overlays,
|
|
88
|
+
transitions, exits. Only reachable states: loading, empty, populated,
|
|
89
|
+
validation, error, disabled, optimistic, destructive, both themes,
|
|
90
|
+
360px and 1280px.
|
|
91
|
+
5. **Load the routed references.**
|
|
92
|
+
|
|
93
|
+
| Need | Load |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Any styling or token decision | `references/tokens.md` |
|
|
96
|
+
| Sizes, radius, spacing, rhythm | `references/type-and-space.md` |
|
|
97
|
+
| Hover, entrance, transitions, reduced motion | `references/motion.md` |
|
|
98
|
+
| Which component, house patterns | `references/components.md` + the component's intent block |
|
|
99
|
+
| Copy, labels, errors, English variant | `references/copy.md` |
|
|
100
|
+
| Which surface, budgets, imagery | `references/surfaces.md` |
|
|
101
|
+
| Any rule by ID, lint status, examples | `references/rules.md` |
|
|
102
|
+
| Before claiming zero visual change | `references/verification.md` |
|
|
103
|
+
| Running or judging an intake packet | `references/intake.md` |
|
|
104
|
+
|
|
105
|
+
6. **Decide, then implement.** For each non-mechanical change be able to
|
|
106
|
+
say: what reader problem it solves, why this component, what consequence
|
|
107
|
+
the surface must communicate, which rule or exemplar supports it, and
|
|
108
|
+
what the smallest coherent change is.
|
|
109
|
+
7. **Verify.** Lint (`pnpm lint`), typecheck, both themes, compact and wide
|
|
110
|
+
viewports, keyboard order and focus, every materially changed state, long
|
|
111
|
+
content. Token work: snapshot compare, delta stated.
|
|
112
|
+
|
|
113
|
+
## Review output
|
|
114
|
+
|
|
115
|
+
Lead with findings, ordered by reader impact.
|
|
116
|
+
|
|
117
|
+
- **P0** blocks the primary task, severe accessibility failure, or harm the
|
|
118
|
+
reader cannot undo.
|
|
119
|
+
- **P1** likely task failure, misleading consequence, missing critical
|
|
120
|
+
state, major responsive or accessibility defect.
|
|
121
|
+
- **P2** meaningful friction, weak hierarchy, inconsistency, recoverability.
|
|
122
|
+
- **P3** minor craft.
|
|
123
|
+
|
|
124
|
+
Each finding: location (file and line, or rendered), verification status
|
|
125
|
+
(seen rendered, inferred from source), rule ID or source, reader
|
|
126
|
+
consequence, smallest concrete fix.
|
|
127
|
+
|
|
128
|
+
## Skill integrity
|
|
129
|
+
|
|
130
|
+
- Add or change a rule only after the same correction has recurred and a
|
|
131
|
+
human has accepted it. One screenshot, one file or one review comment is
|
|
132
|
+
never a rule by itself.
|
|
133
|
+
- Record scope, rule, why, exceptions, source and a bad and good example
|
|
134
|
+
(`references/rules.md` format).
|
|
135
|
+
- Prefer the narrowest destination: a token or contract entry, a lint rule,
|
|
136
|
+
a rule record, an exemplar, or a coverage gap. Deterministic checks stay
|
|
137
|
+
mechanical; judgment stays in prose with its evidence.
|
|
138
|
+
- New evidence enters through the intake loop (`references/intake.md`):
|
|
139
|
+
`ds-intake` collects, the agent proposes in the packet, a human reviews
|
|
140
|
+
and accepts.
|
|
141
|
+
- Keep `coverage-gaps.md` honest. A missing rule is not a licence to invent
|
|
142
|
+
one; it is a decision to raise.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Coverage gaps
|
|
2
|
+
|
|
3
|
+
Decisions we do not have a standard for yet, and rules that exist in prose
|
|
4
|
+
but could be code. A gap is a decision to raise, not a licence to invent.
|
|
5
|
+
|
|
6
|
+
## Lint candidates (checkable by code, not written)
|
|
7
|
+
|
|
8
|
+
Shipped in 0.4.0 as `design-system/*` rules: no-colour-literal,
|
|
9
|
+
no-arbitrary-clamp, no-dark-pairs, no-radius-literal, no-stock-palette,
|
|
10
|
+
focus-visible, on-dark-ramp, no-em-dash. Still candidates:
|
|
11
|
+
|
|
12
|
+
- A `className` on a package component that overrides its colour, radius or
|
|
13
|
+
shadow (layout classes allowed). Needs the imported component names.
|
|
14
|
+
- rule/no-dark-pairs, second form: a `dark:` token utility beside its light
|
|
15
|
+
twin (`bg-band dark:bg-dark`), which usually means a missing token.
|
|
16
|
+
- rule/voice-bans as a word list over string literals in JSX.
|
|
17
|
+
- Warnings to turn into errors once each product is clean: no-stock-palette
|
|
18
|
+
(BirthGuide 48, birthplans 5), focus-visible (24, 2), no-em-dash (47, 23).
|
|
19
|
+
|
|
20
|
+
## Missing decisions
|
|
21
|
+
|
|
22
|
+
- Loading, empty and error state patterns for engagement surfaces beyond
|
|
23
|
+
optimistic writes: no written standard; each product improvised.
|
|
24
|
+
- Destructive action wording and confirmation shape: not standardised
|
|
25
|
+
(current usage: the questionnaire discard flow, plan deletion).
|
|
26
|
+
- Toast or inline confirmation after a save: no standard.
|
|
27
|
+
- Form validation timing (on blur, on submit) and error placement: follow
|
|
28
|
+
the package's FormMessage, but no written rule.
|
|
29
|
+
- A house table treatment: none; the price tracker and calculators each
|
|
30
|
+
built their own.
|
|
31
|
+
- Illustration style for conversion surfaces: the brand spec's guidance
|
|
32
|
+
predates the warm palette and names colours that are not in production.
|
|
33
|
+
- birthplans.app type-role pass: fourteen clamp literals await convergence
|
|
34
|
+
before its fluid-type lint can switch on.
|
|
35
|
+
- Whether a product may ship a manual theme toggle: today, none do, by
|
|
36
|
+
decision; not recorded as a rule.
|
|
37
|
+
|
|
38
|
+
## Evals
|
|
39
|
+
|
|
40
|
+
None yet. When the exemplars reach ten, build before and after fixtures
|
|
41
|
+
from them and hold two out.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Exemplar: calm the offering cards
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Product: BirthGuide, landing hero offering pair
|
|
5
|
+
Source: BirthGuide commit "feat(landing): calm the offering cards down"
|
|
6
|
+
Rules: rule/one-emphasis-signal, rule/concentric-radii, rule/semantic-first
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
The two hero cards carried four emphasis signals at once: a tinted border,
|
|
11
|
+
a heavy drop shadow, a bold uppercase chip and an accent CTA. Cut to the
|
|
12
|
+
chip and the CTA. Both cards moved onto the same hairline and the same soft
|
|
13
|
+
shadow, and onto the treatment the testimonial cards already used
|
|
14
|
+
(`rounded-20 p-7 shadow-warm-sm`). Type and ornament came down with it:
|
|
15
|
+
icon tiles 54px to 40px, title capped at 24px, the accent semibold tagline
|
|
16
|
+
became a muted caption, accent ticks became muted dots. The comparison band
|
|
17
|
+
took the same surface so the two pairs still read as siblings.
|
|
18
|
+
|
|
19
|
+
## Why it matters
|
|
20
|
+
|
|
21
|
+
Emphasis is a budget. When everything on a card is loud, nothing is. The
|
|
22
|
+
fix was subtraction and reuse of an existing surface, not a new style.
|
|
23
|
+
|
|
24
|
+
## Repeat
|
|
25
|
+
|
|
26
|
+
- Count the signals on a surface before styling it. If more than one, remove.
|
|
27
|
+
- Borrow the surface an adjacent component already uses before inventing one.
|
|
28
|
+
- Bring siblings along so a calmer card does not look like the odd one out.
|
|
29
|
+
|
|
30
|
+
## Avoid
|
|
31
|
+
|
|
32
|
+
- Answering "this card does not stand out" with a stronger border or shadow.
|
|
33
|
+
- A new radius, shadow or tint for one card.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Exemplar: converge near-miss clamp drift onto named roles
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Product: BirthGuide landing, then the package's type roles
|
|
5
|
+
Source: BirthGuide commits "refactor(design): name the fluid type and band rhythm tokens", "fix(landing): converge near-miss clamp drift onto the named tokens", "feat(lint): guard arbitrary fluid type sizes in className"
|
|
6
|
+
Rules: rule/no-arbitrary-clamp
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
A typography audit found five bespoke h1 clamps, eight section-title
|
|
11
|
+
clamps and seven lede clamps, most within a pixel or two of each other.
|
|
12
|
+
The exact current hero scale became `text-display` (byte-identical, with
|
|
13
|
+
its 0.9 line-height companion); section titles and ledes converged onto
|
|
14
|
+
`text-section-title` and `text-lede`, with two approved rewraps checked
|
|
15
|
+
from renders (lede cap 20px to 19px, guides gap 48px to 52px). The final
|
|
16
|
+
CTA kept its bespoke 62px clamp by decision, as did the benched sections
|
|
17
|
+
and the mobile-link h1; each carries an inline disable with the reason.
|
|
18
|
+
Then the lint rule went on so no new clamp literal lands.
|
|
19
|
+
|
|
20
|
+
## Why it matters
|
|
21
|
+
|
|
22
|
+
Near-miss sizes are invisible drift: nobody can say which of eight almost
|
|
23
|
+
identical clamps is the intended one. Naming the role settles it, and the
|
|
24
|
+
lint keeps it settled. The deliberate one-offs stay visible in review
|
|
25
|
+
because the disable states why.
|
|
26
|
+
|
|
27
|
+
## Repeat
|
|
28
|
+
|
|
29
|
+
- Name the role at the exact current value first (zero change), then
|
|
30
|
+
converge the near misses in a separate, render-reviewed step.
|
|
31
|
+
- Turn the lint on only after the codebase is clean or every exception is
|
|
32
|
+
disabled with a reason.
|
|
33
|
+
|
|
34
|
+
## Avoid
|
|
35
|
+
|
|
36
|
+
- Converging by eye in one pass with the rename.
|
|
37
|
+
- Silencing the rule file-wide.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Exemplar: craft pass, concentric radii and button optics
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Product: BirthGuide, then the package's Button
|
|
5
|
+
Source: BirthGuide commits "feat(design): craft pass tier 1: tabular numerals, concentric radii, button optics", "feat(ui): move button primitive from pill to size-scaled rounded-rect" (SPEC_021)
|
|
6
|
+
Rules: rule/concentric-radii, rule/no-radius-literal
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Exact-token radius renames first (`rounded-[26px]` to `4xl`,
|
|
11
|
+
`rounded-[14px]` to `xl`, `rounded-[10px]` to `lg`), zero rendering change.
|
|
12
|
+
Then concentric corners where a rounded child meets a rounded parent, each
|
|
13
|
+
landing on a named token. Tabular numerals on five column surfaces only;
|
|
14
|
+
headline figures stayed proportional. Buttons moved from pills to rounded
|
|
15
|
+
rectangles on a size-scaled corner ladder so a flush button can go
|
|
16
|
+
concentric with its container, and only the icon side of a button tightens
|
|
17
|
+
so icon-plus-text reads centred (the primitive wraps bare text in a span so
|
|
18
|
+
CSS can tell which side the icon is on).
|
|
19
|
+
|
|
20
|
+
## Why it matters
|
|
21
|
+
|
|
22
|
+
The snapshot harness compared identical on all 311 covered keys, which
|
|
23
|
+
proved no incidental drift; the intended changes sat below the tool's
|
|
24
|
+
resolution and were reviewed as source diff plus live probes. Craft work
|
|
25
|
+
and refactor work were separated so each could be verified its own way.
|
|
26
|
+
|
|
27
|
+
## Repeat
|
|
28
|
+
|
|
29
|
+
- Split exact renames (identical snapshot) from intended changes (reviewed
|
|
30
|
+
renders), even inside one spec.
|
|
31
|
+
- Apply the concentric formula only where corners meet.
|
|
32
|
+
|
|
33
|
+
## Avoid
|
|
34
|
+
|
|
35
|
+
- Tabular numerals everywhere; they are for columns.
|
|
36
|
+
- A pill button inside a rounded card; the corners can never be concentric.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Exemplar: dialog close ring only for keyboard users
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Product: BirthGuide, now the package's Dialog
|
|
5
|
+
Source: BirthGuide commit "fix(ui): show the dialog close ring only for keyboard users"
|
|
6
|
+
Rules: rule/focus-visible
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Three PDF-preview modals showed a focus ring on their close button when
|
|
11
|
+
opened with a mouse; the birth-webpage preview did not. The modals were the
|
|
12
|
+
same component. Radix autofocuses the first focusable element on open, and
|
|
13
|
+
the close button carried `focus:ring-2`, so the ring painted wherever the
|
|
14
|
+
dialog body had no controls of its own. Switching to `focus-visible:`
|
|
15
|
+
removed the ring for pointer users on every dialog and kept it for
|
|
16
|
+
keyboard users. Verified both ways: after a click the computed box-shadow
|
|
17
|
+
was none; after Shift+Tab the button matched `:focus-visible` and rendered
|
|
18
|
+
the brand ring.
|
|
19
|
+
|
|
20
|
+
## Why it matters
|
|
21
|
+
|
|
22
|
+
The reader saw a mystery highlight on a button they had not touched. It
|
|
23
|
+
looked like a bug and it was one, in the shadcn default. The comment above
|
|
24
|
+
the element records the reason because `shadcn add dialog` reverts it.
|
|
25
|
+
|
|
26
|
+
## Repeat
|
|
27
|
+
|
|
28
|
+
- Measure before changing: the two modals were compared property by
|
|
29
|
+
property and only the focused element differed.
|
|
30
|
+
- Verify accessibility fixes in both directions, pointer and keyboard.
|
|
31
|
+
- Write the reason next to a deliberate deviation from a generator's default.
|
|
32
|
+
|
|
33
|
+
## Avoid
|
|
34
|
+
|
|
35
|
+
- Fixing one modal. The component is shared; fix it once, upstream.
|
|
36
|
+
- Removing the ring for everyone.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Exemplar: blend the hero glow into the next section
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Product: BirthGuide, landing hero to curriculum boundary
|
|
5
|
+
Source: BirthGuide commit "fix(landing): blend the hero glow into the curriculum section"
|
|
6
|
+
Rules: rule/no-clipped-ambient
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
The hero's ambient glow layer kept `overflow-hidden` because its blobs sit
|
|
11
|
+
past the left and right edges, but the same clip sat at `bottom-0` and cut
|
|
12
|
+
the second blob flat at the hero's last pixel. Measured at 1280x800, the
|
|
13
|
+
boundary carried a luminance step of 16.99 in dark and 5.63 in light,
|
|
14
|
+
exactly on the row where the sections meet. Three changes together: the
|
|
15
|
+
layer extends 280px past the bottom (blob offset plus blur), a mask
|
|
16
|
+
dissolves the overhang across that distance, and the next section takes
|
|
17
|
+
`relative z-10` so the glow paints behind its content.
|
|
18
|
+
|
|
19
|
+
## Why it matters
|
|
20
|
+
|
|
21
|
+
A hard horizontal seam between sections is the kind of defect nobody can
|
|
22
|
+
name but everyone feels. It was invisible in source and obvious once
|
|
23
|
+
measured on the rendered page.
|
|
24
|
+
|
|
25
|
+
## Repeat
|
|
26
|
+
|
|
27
|
+
- Verify ambient effects rendered, at the seam, in both themes; measure the
|
|
28
|
+
luminance step if unsure.
|
|
29
|
+
- Extend the clip past blob plus blur, then mask; do not just remove the clip.
|
|
30
|
+
- Give the following section a stacking context so the overhang stays behind.
|
|
31
|
+
|
|
32
|
+
## Avoid
|
|
33
|
+
|
|
34
|
+
- Shrinking or moving the blob to dodge the clip; the composition changes.
|