@fracazo/design-system 0.2.0 → 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 +366 -8
- package/README.md +77 -11
- 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,319 @@
|
|
|
1
|
+
# Rules
|
|
2
|
+
|
|
3
|
+
Every rule has a stable ID, a scope, the rule, why, exceptions, its source
|
|
4
|
+
and an example pair. `Enforced by` says who catches a violation today:
|
|
5
|
+
`lint` (the package's ESLint plugin, rule `design-system/<id>`; `lint,
|
|
6
|
+
warn` means it ships at warning level until the product is clean),
|
|
7
|
+
`contract` (ds-check-brand), `base` (the house base layer in globals.css),
|
|
8
|
+
`prose` (this skill and review), or `lint candidate` (checkable by code, not
|
|
9
|
+
yet written; see coverage-gaps.md).
|
|
10
|
+
Cite the ID in findings. Add a rule only per SKILL.md's integrity section.
|
|
11
|
+
|
|
12
|
+
## Colour
|
|
13
|
+
|
|
14
|
+
### rule/no-colour-literal
|
|
15
|
+
Scope: any `className` in product code, stories and the showcase.
|
|
16
|
+
Rule: no hex, oklch(), rgb() or hsl() value, including arbitrary utilities
|
|
17
|
+
like `bg-[#fff]`.
|
|
18
|
+
Why: a literal drifts from the brand file and breaks dark mode silently.
|
|
19
|
+
Exceptions: renderers that cannot use CSS variables (react-pdf, email HTML,
|
|
20
|
+
OG images), listed per product in its ESLint config.
|
|
21
|
+
Source: DESIGN.md, Guardrails; guardrails/eslint.ts.
|
|
22
|
+
Enforced by: lint.
|
|
23
|
+
Bad: `className="bg-[#c2727a] text-white"`
|
|
24
|
+
Good: `className="bg-primary text-primary-foreground"`
|
|
25
|
+
|
|
26
|
+
### rule/no-dark-pairs
|
|
27
|
+
Scope: any `className`.
|
|
28
|
+
Rule: never a hand-authored light and dark pair (`bg-x dark:bg-y`) for a
|
|
29
|
+
colour. The token owns both themes; write one class.
|
|
30
|
+
Why: two literals drift independently and the dark half is never reviewed.
|
|
31
|
+
Exceptions: `dark:` on a non-colour property (opacity, display) is fine.
|
|
32
|
+
Source: DESIGN.md, Reject list; BirthGuide SPEC_016.
|
|
33
|
+
Enforced by: lint (the arbitrary-value form). A `dark:` token beside its
|
|
34
|
+
light twin is still prose.
|
|
35
|
+
Bad: `bg-[#E6EFE2] dark:bg-[oklch(0.34_0.045_150)]`
|
|
36
|
+
Good: `bg-chip-1-soft`
|
|
37
|
+
|
|
38
|
+
### rule/on-dark-ramp
|
|
39
|
+
Scope: children of an always-dark surface (`bg-dark`, `bg-dark-2`).
|
|
40
|
+
Rule: text and icons use the mode-constant on-dark ramp (`text-dark-ink` to
|
|
41
|
+
`text-dark-faint-2`, `text-dark-brand` for the one accent), never a
|
|
42
|
+
theme-varying token such as `text-ink` or `text-ink-3`.
|
|
43
|
+
Why: the surface never changes theme, so its text must not; `text-ink` on
|
|
44
|
+
the footer renders espresso on espresso in light mode.
|
|
45
|
+
Exceptions: none.
|
|
46
|
+
Source: DESIGN.md, Colour; BirthGuide usage story.
|
|
47
|
+
Enforced by: lint, warn (a theme-varying text token inside a JSX subtree
|
|
48
|
+
whose root carries `bg-dark`; a nested light surface resets the context).
|
|
49
|
+
Bad: `<footer className="bg-dark"><p className="text-ink">`
|
|
50
|
+
Good: `<footer className="bg-dark"><p className="text-dark-ink-2">`
|
|
51
|
+
|
|
52
|
+
### rule/semantic-first
|
|
53
|
+
Scope: any styling decision.
|
|
54
|
+
Rule: semantic utilities (`bg-card`, `text-muted-foreground`,
|
|
55
|
+
`border-border`) before primitives (`bg-band`, `text-ink-3`); primitives
|
|
56
|
+
only where no role fits.
|
|
57
|
+
Why: semantics move together across products and themes; primitives are
|
|
58
|
+
brand material.
|
|
59
|
+
Exceptions: alternating bands, captions on light surfaces, chips, glows and
|
|
60
|
+
the always-dark family, which have no semantic role by design.
|
|
61
|
+
Source: DESIGN.md, Colour.
|
|
62
|
+
Enforced by: prose.
|
|
63
|
+
Bad: `text-ink-2` for body copy under a heading.
|
|
64
|
+
Good: `text-muted-foreground`.
|
|
65
|
+
|
|
66
|
+
### rule/divergent-six
|
|
67
|
+
Scope: brand files and roles.css.
|
|
68
|
+
Rule: secondary, muted, border, input, muted-foreground and
|
|
69
|
+
accent-foreground hold their own literals per theme. Never alias them to a
|
|
70
|
+
primitive.
|
|
71
|
+
Why: dark tuned them away from the primitives; dark border and input are
|
|
72
|
+
translucent hairlines, nothing like the opaque warm line.
|
|
73
|
+
Exceptions: none.
|
|
74
|
+
Source: DESIGN.md, Tokens; roles.css header.
|
|
75
|
+
Enforced by: contract (the six are required in both light and dark) and
|
|
76
|
+
prose.
|
|
77
|
+
Bad: `--border: var(--line);` in the dark block.
|
|
78
|
+
Good: `--border: oklch(1 0 0 / 10%);`
|
|
79
|
+
|
|
80
|
+
### rule/no-stock-palette
|
|
81
|
+
Scope: product UI (not PDFs or emails).
|
|
82
|
+
Rule: no Tailwind default palette colour (`amber-500`, `blue-600`,
|
|
83
|
+
`gray-200`) in a className.
|
|
84
|
+
Why: it competes with the brand, ignores dark mode and looks generic.
|
|
85
|
+
Exceptions: none in UI. Known violation: BirthGuide `PregnancyProgress.tsx`
|
|
86
|
+
(coverage gap).
|
|
87
|
+
Source: BirthGuide brand spec 9.2; DESIGN.md, Colour.
|
|
88
|
+
Enforced by: lint, warn (BirthGuide has 48 occurrences, mostly in the
|
|
89
|
+
calculator tools; a cleanup spec turns it to error).
|
|
90
|
+
Bad: `bg-amber-100 text-amber-800`
|
|
91
|
+
Good: `bg-highlight-soft text-highlight-ink`
|
|
92
|
+
|
|
93
|
+
### rule/status-colour-means-preference
|
|
94
|
+
Scope: `status-want`, `status-ifnec`, `status-no` and their `-soft` washes.
|
|
95
|
+
Rule: use only to encode a reader's preference state; never as decoration
|
|
96
|
+
or as generic success and danger colours.
|
|
97
|
+
Why: colour that carries meaning must carry only that meaning.
|
|
98
|
+
Exceptions: none. Destructive actions use `destructive`.
|
|
99
|
+
Source: DESIGN.md, Colour; roles.css comment.
|
|
100
|
+
Enforced by: prose.
|
|
101
|
+
Bad: a green `bg-status-want-soft` behind a marketing testimonial.
|
|
102
|
+
Good: the "Want" chip on a preference card.
|
|
103
|
+
|
|
104
|
+
## Radius, type and space
|
|
105
|
+
|
|
106
|
+
### rule/no-radius-literal
|
|
107
|
+
Scope: any `className`.
|
|
108
|
+
Rule: radii are `rounded-sm` to `rounded-4xl` plus `rounded-20`. No
|
|
109
|
+
`rounded-[Npx]`.
|
|
110
|
+
Why: a new radius is a design decision; 34 hand-written 20px radii once
|
|
111
|
+
drifted before `rounded-20` named them.
|
|
112
|
+
Exceptions: none. A genuinely new radius becomes a token first.
|
|
113
|
+
Source: DESIGN.md, Radius.
|
|
114
|
+
Enforced by: lint.
|
|
115
|
+
Bad: `rounded-[14px]`
|
|
116
|
+
Good: `rounded-xl`
|
|
117
|
+
|
|
118
|
+
### rule/concentric-radii
|
|
119
|
+
Scope: a rounded child whose corners meet a rounded parent's.
|
|
120
|
+
Rule: outer radius = inner radius + padding, landing on a named token
|
|
121
|
+
(lg + p-4 = 4xl, md + p-3 = rounded-20, sm + p-4 = 3xl, md + p-2.5 = 2xl,
|
|
122
|
+
sm + p-3 = 2xl).
|
|
123
|
+
Why: concentric corners read as one shape; equal radii at different depths
|
|
124
|
+
read as a mistake.
|
|
125
|
+
Exceptions: corners that never meet; gaps that would need a negative inner
|
|
126
|
+
radius.
|
|
127
|
+
Source: DESIGN.md, Radius; BirthGuide SPEC_021 (exemplar
|
|
128
|
+
concentric-radii-and-button-optics).
|
|
129
|
+
Enforced by: prose.
|
|
130
|
+
Bad: `rounded-lg p-4` parent around a `rounded-lg` child.
|
|
131
|
+
Good: `rounded-4xl p-4` parent around a `rounded-lg` child.
|
|
132
|
+
|
|
133
|
+
### rule/no-arbitrary-clamp
|
|
134
|
+
Scope: any `className`.
|
|
135
|
+
Rule: fluid sizes are the named roles `text-display`, `text-section-title`,
|
|
136
|
+
`text-lede`, `py-band`, `mt-band-gap`. No `text-[clamp(...)]`.
|
|
137
|
+
Why: near-miss clamps drift a few pixels from one another and nobody can
|
|
138
|
+
tell which is intended.
|
|
139
|
+
Exceptions: a deliberate one-off with an inline disable stating why (five
|
|
140
|
+
exist in BirthGuide). birthplans.app has the rule off pending its type-role
|
|
141
|
+
pass; no new clamp literals there either.
|
|
142
|
+
Source: DESIGN.md, Type; exemplar clamp-drift-to-named-roles.
|
|
143
|
+
Enforced by: lint (BirthGuide, starter); prose (birthplans until the pass).
|
|
144
|
+
Bad: `text-[clamp(1.9rem,4vw,3rem)]`
|
|
145
|
+
Good: `text-section-title`
|
|
146
|
+
|
|
147
|
+
### rule/tap-targets
|
|
148
|
+
Scope: interactive elements on coarse pointers.
|
|
149
|
+
Rule: at least 44px; buttons and inputs 48px.
|
|
150
|
+
Why: one-handed use, at 3am, on a phone.
|
|
151
|
+
Exceptions: inline text links.
|
|
152
|
+
Source: PRINCIPLES.md technical rules; DESIGN.md, Accessibility.
|
|
153
|
+
Enforced by: base (`@media (pointer: coarse)` block) for the package
|
|
154
|
+
components; prose for custom controls.
|
|
155
|
+
|
|
156
|
+
### rule/inputs-16px
|
|
157
|
+
Scope: text inputs, selects, textareas.
|
|
158
|
+
Rule: font-size at least 16px.
|
|
159
|
+
Why: iOS Safari zooms into smaller inputs on focus.
|
|
160
|
+
Exceptions: none.
|
|
161
|
+
Source: base layer comment.
|
|
162
|
+
Enforced by: base.
|
|
163
|
+
|
|
164
|
+
## Motion
|
|
165
|
+
|
|
166
|
+
### rule/transition-for-interactive-state
|
|
167
|
+
Scope: hover, selection, expand, collapse and any state the reader can
|
|
168
|
+
reverse mid-animation.
|
|
169
|
+
Rule: CSS transitions, not keyframes.
|
|
170
|
+
Why: transitions retarget when interrupted; keyframes restart from zero.
|
|
171
|
+
Exceptions: one-shot staged sequences such as a hero entrance.
|
|
172
|
+
Source: DESIGN.md, Motion; BirthGuide principles story.
|
|
173
|
+
Enforced by: prose.
|
|
174
|
+
|
|
175
|
+
### rule/entrance-once-per-visit
|
|
176
|
+
Scope: entrance and load animations.
|
|
177
|
+
Rule: play once per visit (session flag set before first paint), render at
|
|
178
|
+
rest on reloads and in-visit navigation, collapse under
|
|
179
|
+
`prefers-reduced-motion`.
|
|
180
|
+
Why: replaying an entrance on every reload reads as a bug and costs the
|
|
181
|
+
reader time.
|
|
182
|
+
Exceptions: none.
|
|
183
|
+
Source: BirthGuide layout.tsx; DESIGN.md, Motion.
|
|
184
|
+
Enforced by: prose.
|
|
185
|
+
|
|
186
|
+
### rule/no-clipped-ambient
|
|
187
|
+
Scope: glow blobs, gradients and other ambient layers.
|
|
188
|
+
Rule: an ambient layer must not be clipped at a section boundary; dissolve
|
|
189
|
+
it with a mask or extend the clip past the blob and its blur.
|
|
190
|
+
Why: a clipped blur draws a hard horizontal seam exactly where two sections
|
|
191
|
+
meet.
|
|
192
|
+
Exceptions: none.
|
|
193
|
+
Source: exemplar hero-glow-seam.
|
|
194
|
+
Enforced by: prose (verify rendered).
|
|
195
|
+
|
|
196
|
+
## Interaction and components
|
|
197
|
+
|
|
198
|
+
### rule/focus-visible
|
|
199
|
+
Scope: any element with a focus ring.
|
|
200
|
+
Rule: use `focus-visible:` for rings, not `focus:`.
|
|
201
|
+
Why: Radix autofocuses the first control on open, so a `focus:` ring paints
|
|
202
|
+
on a modal the reader opened with a mouse. Keyboard users still get the
|
|
203
|
+
ring with `focus-visible:`.
|
|
204
|
+
Exceptions: none. Re-running `shadcn add` reverts the dialog; the reason is
|
|
205
|
+
recorded above the element.
|
|
206
|
+
Source: exemplar dialog-close-focus-visible.
|
|
207
|
+
Enforced by: lint, warn (`focus:ring`, `focus:outline`, `focus:border`).
|
|
208
|
+
The skip link in each layout is a legitimate `focus:` use; disable inline
|
|
209
|
+
with the reason.
|
|
210
|
+
Bad: `focus:ring-2 focus:ring-ring`
|
|
211
|
+
Good: `focus-visible:ring-2 focus-visible:ring-ring`
|
|
212
|
+
|
|
213
|
+
### rule/one-emphasis-signal
|
|
214
|
+
Scope: cards, offers, any surface competing for attention.
|
|
215
|
+
Rule: a surface carries one emphasis signal (a chip, a tinted border, a
|
|
216
|
+
heavy shadow, an accent button, bold uppercase type). When it carries
|
|
217
|
+
several, remove until one remains.
|
|
218
|
+
Why: stacked signals cancel each other and read as shouting.
|
|
219
|
+
Exceptions: the single primary action of a step may sit inside an
|
|
220
|
+
emphasised card.
|
|
221
|
+
Source: exemplar calm-the-offering-cards.
|
|
222
|
+
Enforced by: prose.
|
|
223
|
+
|
|
224
|
+
### rule/house-pattern-for-answers
|
|
225
|
+
Scope: questionnaire and preference capture in both products.
|
|
226
|
+
Rule: enumerable answers use the house icon-card groups (single or multi
|
|
227
|
+
select), not bare radios, checkboxes or selects. A single date uses a
|
|
228
|
+
native date input.
|
|
229
|
+
Why: 44px tappable cards with icons are the product's tested answer
|
|
230
|
+
pattern; the OS date picker beats any custom one on mobile.
|
|
231
|
+
Exceptions: dense utilitarian or admin UI may use RadioGroup, Checkbox,
|
|
232
|
+
Select.
|
|
233
|
+
Source: component intent blocks (radio-group, checkbox, select, calendar).
|
|
234
|
+
Enforced by: prose.
|
|
235
|
+
|
|
236
|
+
### rule/sheet-on-mobile-aside-on-desktop
|
|
237
|
+
Scope: supplementary content beside a flow.
|
|
238
|
+
Rule: a bottom Sheet on mobile; an aside on desktop, split at the
|
|
239
|
+
breakpoint. Dialog only for a focused task that needs full attention.
|
|
240
|
+
Why: a modal is the heaviest surface; supplementary content should not
|
|
241
|
+
take the whole screen on desktop.
|
|
242
|
+
Exceptions: previews and confirmations use Dialog on all sizes.
|
|
243
|
+
Source: component intent blocks (sheet, dialog, popover).
|
|
244
|
+
Enforced by: prose.
|
|
245
|
+
|
|
246
|
+
### rule/no-template-reflexes
|
|
247
|
+
Scope: composition.
|
|
248
|
+
Rule: no centred hero plus three cards by default, no metric boxes, no
|
|
249
|
+
badges as metadata, no nested cards, no decorative gradients or glass, no
|
|
250
|
+
carousels or autoplay, no visible theme switcher.
|
|
251
|
+
Why: these are the shapes a generator reaches for when it has not framed
|
|
252
|
+
the reader's job.
|
|
253
|
+
Exceptions: a card grid when the items are genuinely peer units.
|
|
254
|
+
Source: DESIGN.md, Reject list.
|
|
255
|
+
Enforced by: prose.
|
|
256
|
+
|
|
257
|
+
### rule/no-values-in-stories
|
|
258
|
+
Scope: Storybook stories and the package showcase.
|
|
259
|
+
Rule: stories import components and tokens; they never define a colour,
|
|
260
|
+
spacing or variant of their own.
|
|
261
|
+
Why: Storybook is a consumer; a missing value is a finding, not a local
|
|
262
|
+
fix.
|
|
263
|
+
Exceptions: none.
|
|
264
|
+
Source: BirthGuide usage story.
|
|
265
|
+
Enforced by: lint (colour rule covers stories); prose.
|
|
266
|
+
|
|
267
|
+
## Copy
|
|
268
|
+
|
|
269
|
+
### rule/no-em-dash
|
|
270
|
+
Scope: everything: UI copy, comments, commit messages, generated documents,
|
|
271
|
+
metadata.
|
|
272
|
+
Rule: never an em dash. Commas, colons, full stops, parentheses instead.
|
|
273
|
+
Why: house style across every product and repo.
|
|
274
|
+
Exceptions: none (legacy occurrences are removed when a file is touched).
|
|
275
|
+
Source: every CLAUDE.md; DESIGN.md.
|
|
276
|
+
Enforced by: lint, warn (every U+2014 in a source file, comments
|
|
277
|
+
included); docs and commit messages stay prose.
|
|
278
|
+
|
|
279
|
+
### rule/voice-bans
|
|
280
|
+
Scope: all user-facing writing.
|
|
281
|
+
Rule: no wellness-speak ("your journey", "mama", "you've got this"), no
|
|
282
|
+
filler affirmations ("amazing", "incredible"), no AI marketing words
|
|
283
|
+
("seamless", "empower", "unlock", "cutting-edge", "revolutionise",
|
|
284
|
+
"great question"). Short sentences, one idea each. Read aloud.
|
|
285
|
+
Why: the products earn trust by sounding like a midwife who respects the
|
|
286
|
+
reader.
|
|
287
|
+
Exceptions: none.
|
|
288
|
+
Source: PRINCIPLES.md voice rules; both CLAUDE.md copy sections.
|
|
289
|
+
Enforced by: prose (a word list is a lint candidate).
|
|
290
|
+
|
|
291
|
+
### rule/sentence-case
|
|
292
|
+
Scope: headings, buttons, labels, navigation.
|
|
293
|
+
Rule: sentence case. All caps only on the mono kicker label with wide
|
|
294
|
+
tracking.
|
|
295
|
+
Why: Title Case reads as marketing; all caps reads as shouting.
|
|
296
|
+
Exceptions: proper nouns.
|
|
297
|
+
Source: PRINCIPLES.md; DESIGN.md, Type.
|
|
298
|
+
Enforced by: prose.
|
|
299
|
+
|
|
300
|
+
### rule/english-per-product
|
|
301
|
+
Scope: copy, comments, commit messages.
|
|
302
|
+
Rule: BirthGuide is Australian English (caesarean, labour, colour);
|
|
303
|
+
birthplans.app is US English (cesarean, labor, color, anesthesiologist).
|
|
304
|
+
The package and the starter are Australian English.
|
|
305
|
+
Why: each product speaks to its market; mixing reads as carelessness.
|
|
306
|
+
Exceptions: Tailwind class names stay US spelling everywhere.
|
|
307
|
+
Source: each CLAUDE.md language section.
|
|
308
|
+
Enforced by: prose.
|
|
309
|
+
|
|
310
|
+
### rule/claim-only-what-ships
|
|
311
|
+
Scope: landing and product copy.
|
|
312
|
+
Rule: outcomes and features named in copy are ones the product delivers
|
|
313
|
+
today.
|
|
314
|
+
Why: the reader is deciding under pressure; an overclaim is a broken
|
|
315
|
+
promise at the bedside.
|
|
316
|
+
Exceptions: none.
|
|
317
|
+
Source: BirthGuide landing commits ("make the outcome copy claim only what
|
|
318
|
+
the product delivers").
|
|
319
|
+
Enforced by: prose.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Surfaces
|
|
2
|
+
|
|
3
|
+
Load when: starting any work, to name the scope; and for product-specific
|
|
4
|
+
routes.
|
|
5
|
+
Canonical owner: BirthGuide `_context/PRINCIPLES.md` (scopes and budgets);
|
|
6
|
+
`DESIGN.md`, Two surface scopes and the brand chapters.
|
|
7
|
+
|
|
8
|
+
## Scopes
|
|
9
|
+
|
|
10
|
+
**Engagement**: the product itself. Failure is "it did not load when I
|
|
11
|
+
needed it". Cold start under 1.5s on a three-year-old phone on slow 4G,
|
|
12
|
+
first paint under 1s; no hero imagery, no decorative motion; optimistic
|
|
13
|
+
writes; 44px targets; 16px inputs.
|
|
14
|
+
|
|
15
|
+
**Conversion**: landing, guides, articles. Failure is "it looked cheap" or
|
|
16
|
+
"search buried it". LCP under 2.5s, CLS under 0.1, INP under 200ms, first
|
|
17
|
+
load under 1MB, images under 200KB each and 500KB per page. Illustration
|
|
18
|
+
over photography, custom only; one custom typeface; entrance motion once
|
|
19
|
+
per visit.
|
|
20
|
+
|
|
21
|
+
The hospital case applies to both: a landing page opened on a ward tour is
|
|
22
|
+
still read under pressure.
|
|
23
|
+
|
|
24
|
+
## BirthGuide (Australian English)
|
|
25
|
+
|
|
26
|
+
- Conversion: `/` (hero, offerings, program curriculum, comparison,
|
|
27
|
+
testimonials, pricing, FAQ, footer), `/guides/*`, `/tools/*`.
|
|
28
|
+
- Engagement: `/questionnaire`, `/plan/edit/*`, `/plan/[slug]` (the
|
|
29
|
+
published plan read at the bedside), downloads, `/program/*` sessions,
|
|
30
|
+
`/resume`, `/unsubscribe`.
|
|
31
|
+
- Always-dark: footer and showcase bands. Landing phone mock uses `dark-3`
|
|
32
|
+
and `dark-4`.
|
|
33
|
+
- Served token API: `public/brand.css` (generated; lint fails when stale).
|
|
34
|
+
- Local Storybook is the component gallery for both products.
|
|
35
|
+
|
|
36
|
+
## birthplans.app (US English)
|
|
37
|
+
|
|
38
|
+
- Conversion: `/` (hero, plan comparison, free answers, pricing, FAQ, stat
|
|
39
|
+
band), guides.
|
|
40
|
+
- Engagement: `/questionnaire` (with the did-you-know bar and the
|
|
41
|
+
preference status ramp), `/plan/*` preview and download; one output, the
|
|
42
|
+
PDF.
|
|
43
|
+
- Always-dark: footer.
|
|
44
|
+
- Fluid-type lint rule is off until the type-role pass; no new clamps.
|
|
45
|
+
- No Storybook, no served brand.css.
|
|
46
|
+
|
|
47
|
+
## Starter
|
|
48
|
+
|
|
49
|
+
One proof page. Delete it. Everything else here applies once the product
|
|
50
|
+
has surfaces.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Tokens
|
|
2
|
+
|
|
3
|
+
Load when: any decision that picks a colour, surface, shadow or theme
|
|
4
|
+
behaviour.
|
|
5
|
+
Canonical owner: `css/roles.css` (roles and the brand contract) and the
|
|
6
|
+
product's brand file (values). `DESIGN.md`, Tokens and Colour, explains the
|
|
7
|
+
model. Do not restate values here; read them.
|
|
8
|
+
|
|
9
|
+
## Decide in this order
|
|
10
|
+
|
|
11
|
+
1. Is there a shadcn semantic for it? `bg-background`, `bg-card`,
|
|
12
|
+
`bg-popover`, `bg-primary`, `bg-secondary`, `bg-muted`, `bg-accent`,
|
|
13
|
+
`text-foreground`, `text-muted-foreground`, `border-border`,
|
|
14
|
+
`border-input`, `ring-ring`, `bg-destructive`. Use it
|
|
15
|
+
(rule/semantic-first).
|
|
16
|
+
2. Is the surface an alternating band, a caption on a light surface, an
|
|
17
|
+
accent chip, a glow, or a highlight pill? Use the primitive that names
|
|
18
|
+
it: `bg-band`, `bg-band-2`, `bg-surface-2`, `text-ink-3`, `chip-1/2/3`,
|
|
19
|
+
`glow-1/2`, `highlight`.
|
|
20
|
+
3. Is the surface always dark in both themes? `bg-dark` or `bg-dark-2`, and
|
|
21
|
+
every foreground on it from the on-dark ramp (rule/on-dark-ramp).
|
|
22
|
+
4. Does the colour encode a preference state? `status-*`
|
|
23
|
+
(rule/status-colour-means-preference).
|
|
24
|
+
5. None of the above: the value does not exist. Do not inline it. Raise a
|
|
25
|
+
role proposal in the design-system repo (minor version, contract entry,
|
|
26
|
+
both brand files).
|
|
27
|
+
|
|
28
|
+
## Things that look wrong and are not
|
|
29
|
+
|
|
30
|
+
- The six divergent semantics hold literals in both themes and are not
|
|
31
|
+
aliased (rule/divergent-six). Dark `border` and `input` are translucent
|
|
32
|
+
white hairlines.
|
|
33
|
+
- `--dark-3` and `--dark-4` exist for the landing phone mock; nothing else
|
|
34
|
+
should use them.
|
|
35
|
+
- `--headline-accent` behaves differently per brand (BirthGuide: brand ink
|
|
36
|
+
in light, sand in dark; birthplans: sand, applied only under `dark:`).
|
|
37
|
+
- `chip-3` is the sister product's tint in each brand. Do not "correct" it.
|
|
38
|
+
- `--dark-soft-2`, `--dark-faint`, `--dark-faint-2` are near-identical greys
|
|
39
|
+
kept on purpose (exact-match discipline). Collapsing them needs Alex.
|
|
40
|
+
|
|
41
|
+
## Shadows
|
|
42
|
+
|
|
43
|
+
`shadow-card`, `shadow-card-hover`, `shadow-card-selected` replace flat
|
|
44
|
+
borders with layered depth; selected embeds `var(--primary)` as a ring.
|
|
45
|
+
`shadow-warm-sm/md/lg` for landing cards (dark swaps to black-based
|
|
46
|
+
shadows). `shadow-bar` under a floating bottom bar. Spacing separates
|
|
47
|
+
before a border does. No raw `shadow-[...]`.
|
|
48
|
+
|
|
49
|
+
## Adding a role
|
|
50
|
+
|
|
51
|
+
A new or renamed role: add to `roles.css` (theme mapping and the contract
|
|
52
|
+
lists), value it in every brand file, bump the package minor, note it in
|
|
53
|
+
the changelog, then bump each product and snapshot compare (additions
|
|
54
|
+
only). A change that a brand must satisfy anew is a major.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Type and space
|
|
2
|
+
|
|
3
|
+
Load when: sizes, weights, radii, spacing rhythm, alignment.
|
|
4
|
+
Canonical owner: `css/roles.css` (fluid type roles, radius ramp, band
|
|
5
|
+
rhythm); `DESIGN.md`, Radius and Type.
|
|
6
|
+
|
|
7
|
+
## Type
|
|
8
|
+
|
|
9
|
+
- One sans per product, one mono, mapped in the brand file's `@theme` block
|
|
10
|
+
from `next/font` variables. `display: optional` so text never blocks.
|
|
11
|
+
- Headings: weight 600, tracking -0.02em, `text-wrap: balance` (base
|
|
12
|
+
layer). Paragraphs `text-wrap: pretty`.
|
|
13
|
+
- Fluid sizes are roles (rule/no-arbitrary-clamp): `text-display` (with its
|
|
14
|
+
0.9 line-height companion) for the one display headline,
|
|
15
|
+
`text-section-title` for section headings, `text-lede` for the lede
|
|
16
|
+
(leading set at the use site). Everything else is the static scale.
|
|
17
|
+
- Numbers in columns: `tabular-nums`. Headline figures stay proportional.
|
|
18
|
+
- Mono is for code, identifiers and the small uppercase kicker
|
|
19
|
+
(`font-mono text-xs font-semibold uppercase tracking-[0.16em]
|
|
20
|
+
text-ink-3`); nothing else, and nothing else is all caps
|
|
21
|
+
(rule/sentence-case).
|
|
22
|
+
- Prose measure around 60 to 68 characters (`max-w-prose`).
|
|
23
|
+
|
|
24
|
+
## Radius
|
|
25
|
+
|
|
26
|
+
Ramp from the brand's `--radius`: `sm` to `4xl`, plus `rounded-20`
|
|
27
|
+
(rule/no-radius-literal). Concentric corners: outer = inner + padding on a
|
|
28
|
+
named token (rule/concentric-radii). Text buttons sit on a size-scaled
|
|
29
|
+
ladder (xs sm, sm md, default lg, lg xl); icon-only buttons are circles.
|
|
30
|
+
|
|
31
|
+
## Rhythm
|
|
32
|
+
|
|
33
|
+
Landing sections: `py-band` for section verticals, `mt-band-gap` between a
|
|
34
|
+
section header and its content. Inside a card the padding steps are `p-4`,
|
|
35
|
+
`p-6`, `p-7` (the house card is `rounded-20 p-7 shadow-warm-sm`). Spacing
|
|
36
|
+
off the 4px grid is a smell; check whether an existing step fits first.
|
|
37
|
+
|
|
38
|
+
## Alignment
|
|
39
|
+
|
|
40
|
+
Shared baselines and unmistakable gutters. A narrow table in a wide
|
|
41
|
+
section, or a card that is the only misaligned element in a row, is a
|
|
42
|
+
defect at P2.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
Load when: before claiming a change is safe, complete or zero-visual.
|
|
4
|
+
Canonical owner: each product's CLAUDE.md verification section and
|
|
5
|
+
`scripts/design-snapshot.ts`; the package's `pnpm check` and `pnpm build`.
|
|
6
|
+
|
|
7
|
+
## Every UI change
|
|
8
|
+
|
|
9
|
+
1. `pnpm lint` (ESLint guardrails plus the brand contract; BirthGuide also
|
|
10
|
+
checks the served brand.css).
|
|
11
|
+
2. `pnpm typecheck` or `tsc --noEmit`; `pnpm build`.
|
|
12
|
+
3. Render it: both themes, 360px and 1280px, every materially changed
|
|
13
|
+
state, keyboard order and focus, long content. Say what you saw; never
|
|
14
|
+
claim visual verification from source alone.
|
|
15
|
+
|
|
16
|
+
## Token, role or component work
|
|
17
|
+
|
|
18
|
+
- Snapshot before editing (`pnpm design:snapshot baseline.json`), after
|
|
19
|
+
(`after.json`), compare. A refactor compares identical. An intentional
|
|
20
|
+
change shows only the keys you meant to move; state the count.
|
|
21
|
+
- Renames: apply the rename map to the baseline's keys and require zero
|
|
22
|
+
value differences (a small comparer script did this twice on 5 Sep 2026;
|
|
23
|
+
map order matters, longest names first).
|
|
24
|
+
- Clear `.next` after restructuring the CSS import graph; stop any other
|
|
25
|
+
dev server on the same checkout first.
|
|
26
|
+
- `design-baseline.json` is the committed intended state. An intentional
|
|
27
|
+
change regenerates it in the same branch and the commit names the delta.
|
|
28
|
+
|
|
29
|
+
## Package release
|
|
30
|
+
|
|
31
|
+
`pnpm check`, `pnpm build`, `ds-check-brand` against both product brand
|
|
32
|
+
files, `ds-build-brand-css --check` against BirthGuide's committed
|
|
33
|
+
`public/brand.css`. Alex publishes (browser auth); tag `vX.Y.Z`. After a
|
|
34
|
+
merge that adds devDependencies, `pnpm install` in the main checkout before
|
|
35
|
+
publishing.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
A new product on `@fracazo/design-system`, written by `ds-init` from the
|
|
6
|
+
package's `template/`: Next 16, Tailwind v4, the package, one blank brand
|
|
7
|
+
file, the guardrails on. Rewrite the top of this file to describe the
|
|
8
|
+
product and keep the rules below.
|
|
9
|
+
|
|
10
|
+
## Read first
|
|
11
|
+
|
|
12
|
+
When shaping, building, reviewing or writing copy for user-facing UI, load
|
|
13
|
+
`node_modules/@fracazo/design-system/skills/product-design/SKILL.md` first
|
|
14
|
+
It
|
|
15
|
+
names the request mode, routes to the reference that applies and cites rules
|
|
16
|
+
by stable ID. Skip it for backend-only work, telemetry, generated files and
|
|
17
|
+
tests with no shipped UI.
|
|
18
|
+
|
|
19
|
+
`node_modules/@fracazo/design-system/DESIGN.md` is the written authority:
|
|
20
|
+
who the reader is, the priority order, how a page is composed, the reject
|
|
21
|
+
list. `css/roles.css` in the same package holds the roles and the brand
|
|
22
|
+
contract. The component intent lives in each component's source.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- The brand lives in `src/system/brands/*.css` and nowhere else. No colour
|
|
27
|
+
literal, radius literal or clamp() size in a `className`; ESLint errors on
|
|
28
|
+
them. A value the roles do not cover is a proposal for the design-system
|
|
29
|
+
repo, not a local addition.
|
|
30
|
+
- Components come from `@fracazo/design-system/ui/*`. Never copy one into
|
|
31
|
+
this repo to change it; change it upstream and bump the dependency. A
|
|
32
|
+
component that must import app code is the exception and stays here.
|
|
33
|
+
- `pnpm lint` runs ESLint and the brand contract check and must pass before
|
|
34
|
+
a merge. `pnpm typecheck` and `pnpm build` too.
|
|
35
|
+
- Never use em dashes, anywhere. Commas, colons, full stops instead.
|
|
36
|
+
- Feature branch, then fast-forward merge to `main`; no PRs. Conventional
|
|
37
|
+
Commits, present tense.
|
|
38
|
+
- Do not add dependencies without discussing first.
|
|
39
|
+
|
|
40
|
+
## Tooling notes
|
|
41
|
+
|
|
42
|
+
- pnpm 11: `pnpm-workspace.yaml` approves the native build scripts and
|
|
43
|
+
excludes `@fracazo/design-system` from the minimum-release-age gate, so a
|
|
44
|
+
fresh release of the package installs the day it ships.
|
|
45
|
+
- The site follows the system colour scheme; there is no manual toggle. The
|
|
46
|
+
inline script in `layout.tsx` sets `.dark` before first paint and
|
|
47
|
+
`ThemeSync` follows live changes.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# starter
|
|
2
|
+
|
|
3
|
+
A product on [`@fracazo/design-system`](https://github.com/fracazo/design-system),
|
|
4
|
+
written by `ds-init`. Rename this file's title, then follow the numbered
|
|
5
|
+
steps in the package README under "Start a product".
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Before every merge:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm lint && pnpm typecheck && pnpm build
|
|
16
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
import { designSystemGuardrails } from "@fracazo/design-system/eslint";
|
|
5
|
+
|
|
6
|
+
const eslintConfig = defineConfig([
|
|
7
|
+
...nextVitals,
|
|
8
|
+
...nextTs,
|
|
9
|
+
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
|
|
10
|
+
// Both design system guardrails, on from day one: no raw colour values and
|
|
11
|
+
// no arbitrary clamp() type sizes in a className. Add an exemption only for
|
|
12
|
+
// a renderer that genuinely cannot use CSS variables (react-pdf, email HTML,
|
|
13
|
+
// OG images), and say why in a comment next to it.
|
|
14
|
+
designSystemGuardrails({
|
|
15
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
16
|
+
ignores: [],
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files (can opt-in for committing if needed)
|
|
34
|
+
.env*
|
|
35
|
+
|
|
36
|
+
# vercel
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
# typescript
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
next-env.d.ts
|
|
42
|
+
|
|
43
|
+
# Claude Code local tooling
|
|
44
|
+
.claude/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint && pnpm brand:contract",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"brand:contract": "ds-check-brand src/system/brands/starter.css",
|
|
12
|
+
"brand:build": "ds-build-brand-css --brand src/system/brands/starter.css --out public/brand.css --name Starter --url https://example.com"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@dnd-kit/core": "^6.3.1",
|
|
16
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
17
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
18
|
+
"@fracazo/design-system": "^0.6.0",
|
|
19
|
+
"class-variance-authority": "^0.7.1",
|
|
20
|
+
"clsx": "^2.1.1",
|
|
21
|
+
"lucide-react": "^0.577.0",
|
|
22
|
+
"next": "16.1.6",
|
|
23
|
+
"radix-ui": "^1.6.7",
|
|
24
|
+
"react": "19.2.3",
|
|
25
|
+
"react-day-picker": "^9.14.0",
|
|
26
|
+
"react-dom": "19.2.3",
|
|
27
|
+
"react-hook-form": "^7.87.0",
|
|
28
|
+
"tailwind-merge": "^3.6.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tailwindcss/postcss": "^4",
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"@types/react": "^19",
|
|
34
|
+
"@types/react-dom": "^19",
|
|
35
|
+
"eslint": "^9",
|
|
36
|
+
"eslint-config-next": "16.1.6",
|
|
37
|
+
"tailwindcss": "^4",
|
|
38
|
+
"typescript": "^5"
|
|
39
|
+
}
|
|
40
|
+
}
|