@fracazo/design-system 0.1.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 +12 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/css/roles.css +206 -0
- package/demo/index.html +101 -0
- package/dist/guardrails/build-brand-css.d.ts +2 -0
- package/dist/guardrails/build-brand-css.js +126 -0
- package/dist/guardrails/check-brand.d.ts +2 -0
- package/dist/guardrails/check-brand.js +73 -0
- package/dist/guardrails/eslint.d.ts +21 -0
- package/dist/guardrails/eslint.js +51 -0
- package/package.json +45 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# DESIGN.md
|
|
2
|
+
|
|
3
|
+
The written authority for products built on `@fracazo/design-system`: what
|
|
4
|
+
the reader's job is, how a page is composed, what the system refuses to
|
|
5
|
+
ship. Authored in the next phase. Until then the machine-readable parts
|
|
6
|
+
already exist and are enforced:
|
|
7
|
+
|
|
8
|
+
- The brand contract at the top of `css/roles.css`, held by `ds-check-brand`.
|
|
9
|
+
- The two ESLint guardrails in `@fracazo/design-system/eslint`.
|
|
10
|
+
|
|
11
|
+
Prose rules will be added here only when a correction recurs. The
|
|
12
|
+
stylesheet does the visual work; this file exists for judgment.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Fracazo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @fracazo/design-system
|
|
2
|
+
|
|
3
|
+
Roles, a brand contract and guardrails for a warm, evidence-led product
|
|
4
|
+
design system. The package ships the half of the system that is the same
|
|
5
|
+
for every product; each product supplies one brand file with its values.
|
|
6
|
+
Change the brand file and the whole product re-skins; the roles never move.
|
|
7
|
+
|
|
8
|
+
Built for [BirthGuide](https://birthguide.com.au) and
|
|
9
|
+
[birthplans.app](https://www.birthplans.app), designed to start the next
|
|
10
|
+
product from.
|
|
11
|
+
|
|
12
|
+
## What is in the package
|
|
13
|
+
|
|
14
|
+
| Path | What it is |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `css/roles.css` | The system: the dark variant, the Tailwind v4 `@theme` mapping, the radius ramp, fluid type roles, band rhythm, the eight aliasing semantics, and the **brand contract** at the top |
|
|
17
|
+
| `ds-check-brand` | Holds a brand file to the contract: nothing missing, nothing extra |
|
|
18
|
+
| `ds-build-brand-css` | Composes the plain-CSS token file a product serves publicly (e.g. `/brand.css`) |
|
|
19
|
+
| `@fracazo/design-system/eslint` | Two guardrails: no raw colours and no arbitrary fluid type sizes in a `className` |
|
|
20
|
+
| `demo/index.html` | A showcase page that renders the roles in both modes off a served `/brand.css` |
|
|
21
|
+
| `DESIGN.md` | The written authority: judgment, composition, the rejection list. Authored next |
|
|
22
|
+
|
|
23
|
+
Brand files live in each product repo, not here. The contract is what keeps
|
|
24
|
+
them honest.
|
|
25
|
+
|
|
26
|
+
## Consume it
|
|
27
|
+
|
|
28
|
+
Install (Tailwind v4 is a peer dependency):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @fracazo/design-system
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Import the roles, then exactly one brand file, at the top of your global
|
|
35
|
+
stylesheet. Order matters: roles first.
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
@import "tailwindcss";
|
|
39
|
+
@import "@fracazo/design-system/roles.css";
|
|
40
|
+
@import "./system/brands/my-product.css";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Write the brand file to the contract printed at the top of `roles.css`: one
|
|
44
|
+
`@theme inline` block for the typeface mapping, one `:root, .force-light`
|
|
45
|
+
block with every light value, one `.dark` block with every theme-varying
|
|
46
|
+
value. Then wire the checks into your lint chain:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"scripts": {
|
|
51
|
+
"brand:contract": "ds-check-brand src/system/brands/my-product.css",
|
|
52
|
+
"brand:build": "ds-build-brand-css --brand src/system/brands/my-product.css --out public/brand.css --name MyProduct --url https://my-product.example",
|
|
53
|
+
"brand:check": "ds-build-brand-css --brand src/system/brands/my-product.css --out public/brand.css --name MyProduct --url https://my-product.example --check",
|
|
54
|
+
"lint": "eslint && pnpm brand:check && pnpm brand:contract"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
And the ESLint guardrails, with your own exemptions for renderers that
|
|
60
|
+
genuinely cannot use CSS variables (PDF and email builders, OG images):
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { designSystemGuardrails } from '@fracazo/design-system/eslint'
|
|
64
|
+
|
|
65
|
+
export default defineConfig([
|
|
66
|
+
// ...your base config
|
|
67
|
+
designSystemGuardrails({
|
|
68
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
69
|
+
ignores: ['src/components/pdf/**', 'src/lib/email.ts'],
|
|
70
|
+
}),
|
|
71
|
+
])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## The two tiers, in one paragraph
|
|
75
|
+
|
|
76
|
+
Primitives hold literals (`--brand`, `--band`, `--ink`) and live in the brand
|
|
77
|
+
file. Semantics either alias a primitive via `var()` in `roles.css` (the
|
|
78
|
+
eight that are identical in light and dark: foreground, card, popover,
|
|
79
|
+
primary, accent, ring and their foregrounds) or hold a brand-tuned literal
|
|
80
|
+
in the brand file (the six that diverge in dark: secondary, muted, border,
|
|
81
|
+
input, muted-foreground, accent-foreground). Dark border and input as
|
|
82
|
+
translucent hairlines is a design decision, not duplication; never "fix" a
|
|
83
|
+
divergent semantic by aliasing it.
|
|
84
|
+
|
|
85
|
+
## Versioning
|
|
86
|
+
|
|
87
|
+
Semantic versioning by hand. A new role or a renamed role is a minor bump
|
|
88
|
+
and a note in the changelog; a changed contract that a brand file must
|
|
89
|
+
satisfy anew is a major bump.
|
|
90
|
+
|
|
91
|
+
## Licence
|
|
92
|
+
|
|
93
|
+
MIT.
|
package/css/roles.css
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
Design system roles (src/system/roles.css)
|
|
3
|
+
|
|
4
|
+
The brand-agnostic half of the system: every role the app can name, and
|
|
5
|
+
no brand literal anywhere in this file. Values come from one brand file
|
|
6
|
+
that lives in the product repo and satisfies the contract below. The
|
|
7
|
+
product stylesheet imports this file first, then exactly one brand.
|
|
8
|
+
|
|
9
|
+
Two tiers. Primitives (--brand, --band, --ink ...) hold literals and live
|
|
10
|
+
in the brand file. Semantics either alias a primitive via var() here (the
|
|
11
|
+
eight below, identical in light and dark) or hold a brand-tuned literal in
|
|
12
|
+
the brand file (the six divergent ones: secondary, muted, border, input,
|
|
13
|
+
muted-foreground, accent-foreground). Dark border/input as translucent
|
|
14
|
+
hairlines is a design decision, not duplication; never "fix" a divergent
|
|
15
|
+
semantic by aliasing it.
|
|
16
|
+
|
|
17
|
+
Brand contract. ds-check-brand reads these lists and fails the lint when
|
|
18
|
+
a brand file defines less or more.
|
|
19
|
+
|
|
20
|
+
brand-contract:theme
|
|
21
|
+
--font-sans --font-mono
|
|
22
|
+
brand-contract:light
|
|
23
|
+
--radius
|
|
24
|
+
--card-shadow --card-shadow-hover --card-shadow-selected
|
|
25
|
+
--background --primary-foreground --secondary --secondary-foreground
|
|
26
|
+
--muted --muted-foreground --accent-foreground --destructive
|
|
27
|
+
--border --input
|
|
28
|
+
--band --band-2 --surface --surface-2 --ink --ink-2 --ink-3
|
|
29
|
+
--line --line-2 --brand --brand-ink --brand-soft --dark-accent
|
|
30
|
+
--headline-accent
|
|
31
|
+
--highlight --highlight-ink --highlight-soft --highlight-deep
|
|
32
|
+
--dark --dark-2 --dark-3 --dark-4 --dark-line --dark-soft
|
|
33
|
+
--dark-ink --dark-ink-2 --dark-muted --dark-soft-2 --dark-faint
|
|
34
|
+
--dark-faint-2 --dark-brand
|
|
35
|
+
--chip-1-soft --chip-1-ink --chip-2-soft --chip-2-ink
|
|
36
|
+
--chip-3-soft --chip-3-ink --glow-1 --glow-2
|
|
37
|
+
--status-want --status-want-soft --status-ifnec --status-ifnec-soft
|
|
38
|
+
--status-no --status-no-soft
|
|
39
|
+
--bar-shadow --warm-shadow-sm --warm-shadow-md --warm-shadow-lg
|
|
40
|
+
brand-contract:dark
|
|
41
|
+
--card-shadow --card-shadow-hover --card-shadow-selected
|
|
42
|
+
--background --primary-foreground --secondary --secondary-foreground
|
|
43
|
+
--muted --muted-foreground --accent-foreground --destructive
|
|
44
|
+
--border --input
|
|
45
|
+
--band --band-2 --surface --surface-2 --ink --ink-2 --ink-3
|
|
46
|
+
--line --line-2 --brand --brand-ink --brand-soft --dark-accent
|
|
47
|
+
--headline-accent
|
|
48
|
+
--highlight-soft --highlight-deep
|
|
49
|
+
--dark --dark-2 --dark-line --dark-soft
|
|
50
|
+
--chip-1-soft --chip-1-ink --chip-2-soft --chip-2-ink
|
|
51
|
+
--chip-3-soft --chip-3-ink --glow-1 --glow-2
|
|
52
|
+
--status-want --status-want-soft --status-no --status-no-soft
|
|
53
|
+
--warm-shadow-sm --warm-shadow-md --warm-shadow-lg
|
|
54
|
+
brand-contract:end
|
|
55
|
+
|
|
56
|
+
Roles absent from the dark list are mode-constant by design: the radius
|
|
57
|
+
base, the NEW badge (--highlight, --highlight-ink), the phone bezel
|
|
58
|
+
(--dark-3, --dark-4), the on-dark text ramp (--dark-ink ... --dark-brand)
|
|
59
|
+
and the bottom-bar shadow. Always-dark surfaces never change theme, so
|
|
60
|
+
neither does their text. The status "if necessary" pair aliases the
|
|
61
|
+
highlight primitive in light and follows it into dark by itself.
|
|
62
|
+
============================================================================= */
|
|
63
|
+
|
|
64
|
+
@custom-variant dark (&:is(.dark *));
|
|
65
|
+
|
|
66
|
+
@theme inline {
|
|
67
|
+
/* shadcn semantics */
|
|
68
|
+
--color-background: var(--background);
|
|
69
|
+
--color-foreground: var(--foreground);
|
|
70
|
+
--color-ring: var(--ring);
|
|
71
|
+
--color-input: var(--input);
|
|
72
|
+
--color-border: var(--border);
|
|
73
|
+
--color-destructive: var(--destructive);
|
|
74
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
75
|
+
--color-accent: var(--accent);
|
|
76
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
77
|
+
--color-muted: var(--muted);
|
|
78
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
79
|
+
--color-secondary: var(--secondary);
|
|
80
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
81
|
+
--color-primary: var(--primary);
|
|
82
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
83
|
+
--color-popover: var(--popover);
|
|
84
|
+
--color-card-foreground: var(--card-foreground);
|
|
85
|
+
--color-card: var(--card);
|
|
86
|
+
|
|
87
|
+
/* Radius ramp, relative to the brand's --radius base */
|
|
88
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
89
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
90
|
+
--radius-lg: var(--radius);
|
|
91
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
92
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
93
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
94
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
95
|
+
/* Named token for the 20px radius used ~34× as rounded-[20px]. Absolute
|
|
96
|
+
value (not on the relative --radius calc ramp) so it resolves to exactly
|
|
97
|
+
20px, identical to the arbitrary value it replaces. Utility: rounded-20. */
|
|
98
|
+
--radius-20: 20px;
|
|
99
|
+
|
|
100
|
+
/* ── Fluid type roles (SPEC_017): conversion-surface headline scale ───
|
|
101
|
+
Byte-identical to the arbitrary clamp() literals they replace.
|
|
102
|
+
text-section-title carries its line-height via the companion token;
|
|
103
|
+
text-lede deliberately does not, so per-use leading utilities
|
|
104
|
+
(e.g. leading-relaxed) still apply where they exist. */
|
|
105
|
+
--text-display: clamp(2.25rem, 5vw, 3.4rem);
|
|
106
|
+
--text-display--line-height: 0.9;
|
|
107
|
+
--text-section-title: clamp(1.875rem, 4.1vw, 3.0625rem);
|
|
108
|
+
--text-section-title--line-height: 1.05;
|
|
109
|
+
--text-lede: clamp(1.0625rem, 1.6vw, 1.1875rem);
|
|
110
|
+
|
|
111
|
+
/* ── Band rhythm (SPEC_017): landing section verticals ────────────────
|
|
112
|
+
band = section py; band-gap = section header to content mt. */
|
|
113
|
+
--spacing-band: clamp(4.5rem, 9vw, 7.75rem);
|
|
114
|
+
--spacing-band-gap: clamp(2.25rem, 4vw, 3.25rem);
|
|
115
|
+
|
|
116
|
+
/* Shadow-border utilities: shadow-card, shadow-card-hover, shadow-card-selected */
|
|
117
|
+
--shadow-card: var(--card-shadow);
|
|
118
|
+
--shadow-card-hover: var(--card-shadow-hover);
|
|
119
|
+
--shadow-card-selected: var(--card-shadow-selected);
|
|
120
|
+
|
|
121
|
+
/* ── Primitive palette roles ──────────────────────────────────────────
|
|
122
|
+
Theme-aware: each utility maps to a custom property the brand file
|
|
123
|
+
defines under :root / .force-light (light) and overrides in .dark.
|
|
124
|
+
Use the .force-light class to pin a subtree to light, e.g. document
|
|
125
|
+
previews that represent printed white paper. */
|
|
126
|
+
--color-band: var(--band);
|
|
127
|
+
--color-band-2: var(--band-2);
|
|
128
|
+
--color-surface: var(--surface);
|
|
129
|
+
--color-surface-2: var(--surface-2);
|
|
130
|
+
--color-ink: var(--ink);
|
|
131
|
+
--color-ink-2: var(--ink-2);
|
|
132
|
+
--color-ink-3: var(--ink-3);
|
|
133
|
+
--color-line: var(--line);
|
|
134
|
+
--color-line-2: var(--line-2);
|
|
135
|
+
--color-brand: var(--brand);
|
|
136
|
+
--color-brand-ink: var(--brand-ink);
|
|
137
|
+
--color-brand-soft: var(--brand-soft);
|
|
138
|
+
--color-dark-accent: var(--dark-accent);
|
|
139
|
+
--color-headline-accent: var(--headline-accent);
|
|
140
|
+
--color-highlight: var(--highlight);
|
|
141
|
+
--color-highlight-ink: var(--highlight-ink);
|
|
142
|
+
--color-highlight-soft: var(--highlight-soft);
|
|
143
|
+
--color-highlight-deep: var(--highlight-deep);
|
|
144
|
+
--color-dark: var(--dark);
|
|
145
|
+
--color-dark-2: var(--dark-2);
|
|
146
|
+
--color-dark-3: var(--dark-3);
|
|
147
|
+
--color-dark-4: var(--dark-4);
|
|
148
|
+
--color-dark-line: var(--dark-line);
|
|
149
|
+
--color-dark-ink: var(--dark-ink);
|
|
150
|
+
--color-dark-ink-2: var(--dark-ink-2);
|
|
151
|
+
--color-dark-muted: var(--dark-muted);
|
|
152
|
+
--color-dark-soft: var(--dark-soft);
|
|
153
|
+
--color-dark-soft-2: var(--dark-soft-2);
|
|
154
|
+
--color-dark-faint: var(--dark-faint);
|
|
155
|
+
--color-dark-faint-2: var(--dark-faint-2);
|
|
156
|
+
--color-dark-brand: var(--dark-brand);
|
|
157
|
+
|
|
158
|
+
/* Stat chips + hero glow blobs (SPEC_016): theme-varying accent tints
|
|
159
|
+
that let the landing components drop their hand-authored dark: pairs. */
|
|
160
|
+
--color-chip-1-soft: var(--chip-1-soft);
|
|
161
|
+
--color-chip-1-ink: var(--chip-1-ink);
|
|
162
|
+
--color-chip-2-soft: var(--chip-2-soft);
|
|
163
|
+
--color-chip-2-ink: var(--chip-2-ink);
|
|
164
|
+
--color-chip-3-soft: var(--chip-3-soft);
|
|
165
|
+
--color-chip-3-ink: var(--chip-3-ink);
|
|
166
|
+
--color-glow-1: var(--glow-1);
|
|
167
|
+
--color-glow-2: var(--glow-2);
|
|
168
|
+
|
|
169
|
+
/* Traffic-light status ramp (Want / If necessary / Don't want) for
|
|
170
|
+
on-screen preference UI. PDFs and previews keep their own hex palette
|
|
171
|
+
in constants.ts. */
|
|
172
|
+
--color-status-want: var(--status-want);
|
|
173
|
+
--color-status-want-soft: var(--status-want-soft);
|
|
174
|
+
--color-status-ifnec: var(--status-ifnec);
|
|
175
|
+
--color-status-ifnec-soft: var(--status-ifnec-soft);
|
|
176
|
+
--color-status-no: var(--status-no);
|
|
177
|
+
--color-status-no-soft: var(--status-no-soft);
|
|
178
|
+
|
|
179
|
+
/* Warm soft shadows for landing cards: shadow-warm-sm / -md / -lg */
|
|
180
|
+
--shadow-warm-sm: var(--warm-shadow-sm);
|
|
181
|
+
--shadow-warm-md: var(--warm-shadow-md);
|
|
182
|
+
--shadow-warm-lg: var(--warm-shadow-lg);
|
|
183
|
+
|
|
184
|
+
/* Floating bottom-bar shadow (upward): shadow-bar */
|
|
185
|
+
--shadow-bar: var(--bar-shadow);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* The eight aliasing semantics, declared once. They recompute on any element
|
|
189
|
+
that redefines the primitive they point at, which is what lets a
|
|
190
|
+
.force-light subtree inside a dark page resolve to light values. */
|
|
191
|
+
:root,
|
|
192
|
+
.force-light {
|
|
193
|
+
color-scheme: light;
|
|
194
|
+
--foreground: var(--ink);
|
|
195
|
+
--card: var(--surface);
|
|
196
|
+
--card-foreground: var(--ink);
|
|
197
|
+
--popover: var(--surface);
|
|
198
|
+
--popover-foreground: var(--ink);
|
|
199
|
+
--primary: var(--brand);
|
|
200
|
+
--accent: var(--brand-soft);
|
|
201
|
+
--ring: var(--brand);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.dark {
|
|
205
|
+
color-scheme: dark;
|
|
206
|
+
}
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- Showcase template from @fracazo/design-system. Serve it from a product
|
|
3
|
+
that publishes its tokens at /brand.css; the copy and the illustrative
|
|
4
|
+
second skin are BirthGuide and birthplans.app, edit them for yours. -->
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>brand.css demo</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400..700&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/brand.css">
|
|
11
|
+
<style>
|
|
12
|
+
body { margin: 0; font-family: 'Hanken Grotesk', system-ui, sans-serif; }
|
|
13
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; min-height: 100vh; }
|
|
14
|
+
.panel { padding: 30px 36px 34px; background: var(--background); color: var(--ink); }
|
|
15
|
+
|
|
16
|
+
/* Illustrative birthplans.app skin: same roles, periwinkle values.
|
|
17
|
+
Exactly the override contract documented in brand.css's header. */
|
|
18
|
+
.skin-bp { --brand: #7280cf; --brand-ink: #5c6cbe; --brand-soft: #e9ecf8; --headline-accent: var(--brand-ink); }
|
|
19
|
+
.dark.skin-bp { --brand: #96a4ea; --brand-ink: #aeb9f0; --brand-soft: #343c66; --headline-accent: #c3cbf5; }
|
|
20
|
+
|
|
21
|
+
.tag { font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--warm-muted); }
|
|
22
|
+
h2 { margin: 14px 0 0; font-size: var(--text-section-title); line-height: var(--text-section-title--line-height); font-weight: 600; letter-spacing: -0.02em; }
|
|
23
|
+
h2 em { font-style: normal; color: var(--headline-accent); }
|
|
24
|
+
.lede { margin: 12px 0 0; max-width: 46ch; font-size: var(--text-lede); line-height: 1.55; color: var(--ink-2); }
|
|
25
|
+
.row { display: flex; align-items: center; gap: 12px; margin-top: 20px; flex-wrap: wrap; }
|
|
26
|
+
.btn { display: inline-block; padding: 11px 20px; border-radius: 999px; background: var(--brand); color: var(--primary-foreground); font-weight: 600; font-size: 15px; }
|
|
27
|
+
.chip { display: inline-block; padding: 7px 14px; border-radius: 999px; background: var(--brand-soft); color: var(--brand-ink); font-weight: 600; font-size: 13px; }
|
|
28
|
+
.card { margin-top: 22px; max-width: 400px; padding: 16px 18px; background: var(--card); border-radius: var(--radius-lg); box-shadow: var(--card-shadow); }
|
|
29
|
+
.card b { display: block; font-size: 14.5px; }
|
|
30
|
+
.card span { display: block; margin-top: 3px; font-size: 13px; color: var(--warm-muted); }
|
|
31
|
+
.swatches { display: flex; gap: 8px; margin-top: 22px; flex-wrap: wrap; }
|
|
32
|
+
.sw { width: 74px; }
|
|
33
|
+
.sw i { display: block; height: 40px; border-radius: 8px; border: 1px solid var(--line); }
|
|
34
|
+
.sw small { display: block; margin-top: 4px; font-size: 10.5px; color: var(--warm-muted); font-family: ui-monospace, monospace; }
|
|
35
|
+
</style>
|
|
36
|
+
<div class="grid">
|
|
37
|
+
<div class="panel">
|
|
38
|
+
<span class="tag">BirthGuide · light</span>
|
|
39
|
+
<h2>Feel <em>calm and prepared</em> for labour and birth.</h2>
|
|
40
|
+
<p class="lede">Every value on this page resolves from brand.css: colours, type roles, radii and shadows. Nothing is styled directly.</p>
|
|
41
|
+
<div class="row"><span class="btn">Explore the program</span><span class="chip">Evidence-based</span></div>
|
|
42
|
+
<div class="card"><b>Card on surface</b><span>shadow-card over the cream page, radius from the ramp</span></div>
|
|
43
|
+
<div class="swatches">
|
|
44
|
+
<div class="sw"><i style="background:var(--brand)"></i><small>brand</small></div>
|
|
45
|
+
<div class="sw"><i style="background:var(--brand-ink)"></i><small>brand-ink</small></div>
|
|
46
|
+
<div class="sw"><i style="background:var(--brand-soft)"></i><small>brand-soft</small></div>
|
|
47
|
+
<div class="sw"><i style="background:var(--sand)"></i><small>sand</small></div>
|
|
48
|
+
<div class="sw"><i style="background:var(--surface)"></i><small>surface</small></div>
|
|
49
|
+
<div class="sw"><i style="background:var(--ink)"></i><small>ink</small></div>
|
|
50
|
+
<div class="sw"><i style="background:var(--amber)"></i><small>amber</small></div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="panel dark">
|
|
54
|
+
<span class="tag">BirthGuide · dark</span>
|
|
55
|
+
<h2>Feel <em>calm and prepared</em> for labour and birth.</h2>
|
|
56
|
+
<p class="lede">The .dark block swaps every token; the markup and the roles are identical to the light panel.</p>
|
|
57
|
+
<div class="row"><span class="btn">Explore the program</span><span class="chip">Evidence-based</span></div>
|
|
58
|
+
<div class="card"><b>Card on surface</b><span>dark shadows lean on black and hairlines</span></div>
|
|
59
|
+
<div class="swatches">
|
|
60
|
+
<div class="sw"><i style="background:var(--brand)"></i><small>brand</small></div>
|
|
61
|
+
<div class="sw"><i style="background:var(--brand-ink)"></i><small>brand-ink</small></div>
|
|
62
|
+
<div class="sw"><i style="background:var(--brand-soft)"></i><small>brand-soft</small></div>
|
|
63
|
+
<div class="sw"><i style="background:var(--sand)"></i><small>sand</small></div>
|
|
64
|
+
<div class="sw"><i style="background:var(--surface)"></i><small>surface</small></div>
|
|
65
|
+
<div class="sw"><i style="background:var(--ink)"></i><small>ink</small></div>
|
|
66
|
+
<div class="sw"><i style="background:var(--amber)"></i><small>amber</small></div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="panel skin-bp">
|
|
70
|
+
<span class="tag">birthplans.app skin · light (illustrative)</span>
|
|
71
|
+
<h2>Your plan, <em>ready to share</em> in minutes.</h2>
|
|
72
|
+
<p class="lede">Same stylesheet, same roles. Only the brand primitives are overridden, exactly as the brand.css header documents.</p>
|
|
73
|
+
<div class="row"><span class="btn">Build your plan</span><span class="chip">Free basic plan</span></div>
|
|
74
|
+
<div class="card"><b>Card on surface</b><span>warm neutrals carry across products unchanged</span></div>
|
|
75
|
+
<div class="swatches">
|
|
76
|
+
<div class="sw"><i style="background:var(--brand)"></i><small>brand</small></div>
|
|
77
|
+
<div class="sw"><i style="background:var(--brand-ink)"></i><small>brand-ink</small></div>
|
|
78
|
+
<div class="sw"><i style="background:var(--brand-soft)"></i><small>brand-soft</small></div>
|
|
79
|
+
<div class="sw"><i style="background:var(--sand)"></i><small>sand</small></div>
|
|
80
|
+
<div class="sw"><i style="background:var(--surface)"></i><small>surface</small></div>
|
|
81
|
+
<div class="sw"><i style="background:var(--ink)"></i><small>ink</small></div>
|
|
82
|
+
<div class="sw"><i style="background:var(--amber)"></i><small>amber</small></div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="panel dark skin-bp">
|
|
86
|
+
<span class="tag">birthplans.app skin · dark (illustrative)</span>
|
|
87
|
+
<h2>Your plan, <em>ready to share</em> in minutes.</h2>
|
|
88
|
+
<p class="lede">The dark skin overrides the same four primitives again; everything else rides the shared espresso palette.</p>
|
|
89
|
+
<div class="row"><span class="btn">Build your plan</span><span class="chip">Free basic plan</span></div>
|
|
90
|
+
<div class="card"><b>Card on surface</b><span>one system, two products, both modes</span></div>
|
|
91
|
+
<div class="swatches">
|
|
92
|
+
<div class="sw"><i style="background:var(--brand)"></i><small>brand</small></div>
|
|
93
|
+
<div class="sw"><i style="background:var(--brand-ink)"></i><small>brand-ink</small></div>
|
|
94
|
+
<div class="sw"><i style="background:var(--brand-soft)"></i><small>brand-soft</small></div>
|
|
95
|
+
<div class="sw"><i style="background:var(--sand)"></i><small>sand</small></div>
|
|
96
|
+
<div class="sw"><i style="background:var(--surface)"></i><small>surface</small></div>
|
|
97
|
+
<div class="sw"><i style="background:var(--ink)"></i><small>ink</small></div>
|
|
98
|
+
<div class="sw"><i style="background:var(--amber)"></i><small>amber</small></div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Brand token stylesheet generator.
|
|
4
|
+
//
|
|
5
|
+
// Composes a plain-CSS token file (the public API a product serves, e.g. at
|
|
6
|
+
// /brand.css) from the package's roles.css and the product's brand file.
|
|
7
|
+
// Commit the output; never edit it by hand.
|
|
8
|
+
//
|
|
9
|
+
// ds-build-brand-css --brand src/system/brands/birthguide.css \
|
|
10
|
+
// --out public/brand.css \
|
|
11
|
+
// --name BirthGuide --url https://birthguide.com.au
|
|
12
|
+
// ds-build-brand-css ... --check exit 1 when the output is stale
|
|
13
|
+
//
|
|
14
|
+
// Published: the roles' aliasing semantics, the brand's values in both
|
|
15
|
+
// modes, and the mode-constant type, spacing and radius tokens re-homed
|
|
16
|
+
// onto :root so they resolve without Tailwind. Not published: the
|
|
17
|
+
// --color-*/--shadow-* @theme aliases (Tailwind utility mappings) and the
|
|
18
|
+
// brand's font mapping (a framework variable; document the family in the
|
|
19
|
+
// brand file instead).
|
|
20
|
+
// =============================================================================
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
function arg(name) {
|
|
24
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
25
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
26
|
+
}
|
|
27
|
+
const brandPath = arg('brand');
|
|
28
|
+
const outPath = arg('out');
|
|
29
|
+
const name = arg('name') ?? 'Brand';
|
|
30
|
+
const url = arg('url') ?? '';
|
|
31
|
+
const check = process.argv.includes('--check');
|
|
32
|
+
if (!brandPath || !outPath) {
|
|
33
|
+
console.error('usage: ds-build-brand-css --brand <brand.css> --out <path> [--name <Name>] [--url <site>] [--check]');
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
const ROLES = path.resolve(import.meta.dirname, '../../css/roles.css');
|
|
37
|
+
const roles = readFileSync(ROLES, 'utf8');
|
|
38
|
+
const brand = readFileSync(path.resolve(brandPath), 'utf8');
|
|
39
|
+
function block(source, file, re, label) {
|
|
40
|
+
const m = source.match(re);
|
|
41
|
+
if (!m) {
|
|
42
|
+
throw new Error(`brand.css generator: the ${label} block was not found in ${file}. ` +
|
|
43
|
+
'Its shape changed; update the generator to match.');
|
|
44
|
+
}
|
|
45
|
+
return m[1];
|
|
46
|
+
}
|
|
47
|
+
const LIGHT = /^:root,\n\.force-light \{\n([\s\S]*?)\n\}/m;
|
|
48
|
+
const DARK = /^\.dark \{\n([\s\S]*?)\n\}/m;
|
|
49
|
+
const THEME = /^@theme inline \{\n([\s\S]*?)\n\}/m;
|
|
50
|
+
const rolesLight = block(roles, 'roles.css', LIGHT, ':root/.force-light');
|
|
51
|
+
const rolesDark = block(roles, 'roles.css', DARK, '.dark');
|
|
52
|
+
const rolesTheme = block(roles, 'roles.css', THEME, '@theme inline');
|
|
53
|
+
const brandLight = block(brand, brandPath, LIGHT, ':root/.force-light');
|
|
54
|
+
const brandDark = block(brand, brandPath, DARK, '.dark');
|
|
55
|
+
const modeConstantTokens = rolesTheme
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter((line) => /^\s*--(?:text|spacing|radius)-[\w-]+:/.test(line))
|
|
58
|
+
.join('\n');
|
|
59
|
+
if (!modeConstantTokens.includes('--text-display')) {
|
|
60
|
+
throw new Error('brand.css generator: no --text-* tokens matched in the roles @theme block.');
|
|
61
|
+
}
|
|
62
|
+
const contract = block(roles, 'roles.css', /(brand-contract:theme[\s\S]*?)\n\s*brand-contract:end/, 'brand contract')
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map((l) => l.replace(/^\s{3}/, ''))
|
|
65
|
+
.join('\n');
|
|
66
|
+
const header = `/* =============================================================================
|
|
67
|
+
${name} brand tokens${url ? `\n ${url.replace(/\/$/, '')}/brand.css` : ''}
|
|
68
|
+
|
|
69
|
+
GENERATED by @fracazo/design-system (ds-build-brand-css) from the
|
|
70
|
+
package's roles.css and this product's brand file. Never edit by hand:
|
|
71
|
+
edit the brand file and rebuild.
|
|
72
|
+
|
|
73
|
+
Two tiers: primitives hold literals (--brand, --band, --ink); semantics
|
|
74
|
+
reference them via var() (--primary, --border). Light values live on
|
|
75
|
+
:root, dark overrides on .dark; set the .dark class on <html> from
|
|
76
|
+
prefers-color-scheme before first paint, or mirror the .dark block inside
|
|
77
|
+
a prefers-color-scheme media query. .force-light pins a subtree to light
|
|
78
|
+
regardless of mode, e.g. document previews that represent printed paper.
|
|
79
|
+
|
|
80
|
+
Skinning: the roles block below is shared by every product on this
|
|
81
|
+
system; a brand supplies every property in the contract with its own
|
|
82
|
+
values, light and dark. The roles stay identical; only the values change.
|
|
83
|
+
|
|
84
|
+
${contract.split('\n').join('\n ')}
|
|
85
|
+
============================================================================= */
|
|
86
|
+
`;
|
|
87
|
+
const output = `${header}
|
|
88
|
+
/* ── Roles: the aliasing semantics, shared by every brand ──────────────── */
|
|
89
|
+
:root,
|
|
90
|
+
.force-light {
|
|
91
|
+
${rolesLight}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dark {
|
|
95
|
+
${rolesDark}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── Brand: ${name} values ────────────────────────────────────────────── */
|
|
99
|
+
:root,
|
|
100
|
+
.force-light {
|
|
101
|
+
${brandLight}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dark {
|
|
105
|
+
${brandDark}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ── Type, band rhythm and radius (mode-constant; consumed as Tailwind
|
|
109
|
+
theme tokens inside the app, plain custom properties out here) ──────── */
|
|
110
|
+
:root {
|
|
111
|
+
${modeConstantTokens}
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
const out = path.resolve(outPath);
|
|
115
|
+
if (check) {
|
|
116
|
+
const current = existsSync(out) ? readFileSync(out, 'utf8') : null;
|
|
117
|
+
if (current !== output) {
|
|
118
|
+
console.error(`${outPath} is stale against the brand file. Rebuild it and commit the result.`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
console.log(`${outPath} is current.`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
writeFileSync(out, output);
|
|
125
|
+
console.log(`written: ${out}`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Brand contract check.
|
|
4
|
+
//
|
|
5
|
+
// roles.css declares the custom properties a brand file must define, in a
|
|
6
|
+
// machine-readable comment block (brand-contract:theme / :light / :dark).
|
|
7
|
+
// This holds every brand file you pass to that contract: nothing missing,
|
|
8
|
+
// nothing extra. Wire it into your lint chain.
|
|
9
|
+
//
|
|
10
|
+
// ds-check-brand src/system/brands/birthguide.css [more brand files]
|
|
11
|
+
// =============================================================================
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
const ROLES = path.resolve(import.meta.dirname, '../../css/roles.css');
|
|
15
|
+
const roles = readFileSync(ROLES, 'utf8');
|
|
16
|
+
function contractList(section) {
|
|
17
|
+
const re = new RegExp(`brand-contract:${section}\\n([\\s\\S]*?)\\n\\s*brand-contract:`);
|
|
18
|
+
const m = roles.match(re);
|
|
19
|
+
if (!m)
|
|
20
|
+
throw new Error(`roles.css: brand-contract:${section} block not found`);
|
|
21
|
+
return new Set(m[1].match(/--[\w-]+/g) ?? []);
|
|
22
|
+
}
|
|
23
|
+
// Custom properties declared inside the first block whose selector matches.
|
|
24
|
+
function declared(css, selectorRe) {
|
|
25
|
+
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
26
|
+
const blockRe = /([^{}]+)\{([^{}]*)\}/g;
|
|
27
|
+
for (const m of stripped.matchAll(blockRe)) {
|
|
28
|
+
const selector = (m[1].split(';').pop() ?? '').trim();
|
|
29
|
+
if (!selectorRe.test(selector))
|
|
30
|
+
continue;
|
|
31
|
+
return new Set([...m[2].matchAll(/(--[\w-]+)\s*:/g)].map((d) => d[1]));
|
|
32
|
+
}
|
|
33
|
+
return new Set();
|
|
34
|
+
}
|
|
35
|
+
function diff(expected, actual) {
|
|
36
|
+
const missing = [...expected].filter((p) => !actual.has(p));
|
|
37
|
+
const extra = [...actual].filter((p) => !expected.has(p));
|
|
38
|
+
return { missing, extra };
|
|
39
|
+
}
|
|
40
|
+
const contract = {
|
|
41
|
+
theme: contractList('theme'),
|
|
42
|
+
light: contractList('light'),
|
|
43
|
+
dark: contractList('dark'),
|
|
44
|
+
};
|
|
45
|
+
const brandFiles = process.argv.slice(2);
|
|
46
|
+
if (brandFiles.length === 0) {
|
|
47
|
+
console.error('usage: ds-check-brand <brand.css> [more brand files]');
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
let failed = false;
|
|
51
|
+
for (const file of brandFiles) {
|
|
52
|
+
const css = readFileSync(path.resolve(file), 'utf8');
|
|
53
|
+
const checks = [
|
|
54
|
+
['@theme inline', contract.theme, declared(css, /^@theme\b/)],
|
|
55
|
+
[':root, .force-light', contract.light, declared(css, /^:root,\s*\.force-light$/)],
|
|
56
|
+
['.dark', contract.dark, declared(css, /^\.dark$/)],
|
|
57
|
+
];
|
|
58
|
+
for (const [label, expected, actual] of checks) {
|
|
59
|
+
const { missing, extra } = diff(expected, actual);
|
|
60
|
+
if (missing.length || extra.length) {
|
|
61
|
+
failed = true;
|
|
62
|
+
console.error(`${file} ${label}:`);
|
|
63
|
+
for (const p of missing)
|
|
64
|
+
console.error(` missing ${p}`);
|
|
65
|
+
for (const p of extra)
|
|
66
|
+
console.error(` extra ${p} (not a role; declare it in roles.css first)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (failed)
|
|
71
|
+
process.exit(1);
|
|
72
|
+
console.log(`brand contract: ${brandFiles.length} brand file(s) satisfy roles.css ` +
|
|
73
|
+
`(${contract.light.size} light, ${contract.dark.size} dark, ${contract.theme.size} theme).`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type Restriction = {
|
|
2
|
+
selector: string;
|
|
3
|
+
message: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const noArbitraryColour: Restriction[];
|
|
6
|
+
export declare const noArbitraryTypeClamp: Restriction[];
|
|
7
|
+
export interface GuardrailOptions {
|
|
8
|
+
/** Glob(s) the rules apply to. Default: src/**\/*.{ts,tsx}. */
|
|
9
|
+
files?: string[];
|
|
10
|
+
/** Glob(s) exempt from the rules: renderers that genuinely cannot use CSS variables. */
|
|
11
|
+
ignores?: string[];
|
|
12
|
+
}
|
|
13
|
+
/** A flat-config block: spread it into your eslint.config array. */
|
|
14
|
+
export declare function designSystemGuardrails({ files, ignores, }?: GuardrailOptions): {
|
|
15
|
+
files: string[];
|
|
16
|
+
ignores: string[];
|
|
17
|
+
rules: {
|
|
18
|
+
'no-restricted-syntax': (string | Restriction)[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// ESLint guardrails.
|
|
3
|
+
//
|
|
4
|
+
// Two rules that keep design decisions in the token layer instead of in
|
|
5
|
+
// component files:
|
|
6
|
+
// 1. No raw colour values in a className (hex, oklch(), rgb(), hsl()),
|
|
7
|
+
// including Tailwind arbitrary utilities like bg-[#fff]. Colours come
|
|
8
|
+
// from the semantic or primitive utilities the roles define.
|
|
9
|
+
// 2. No arbitrary fluid type size in a className (text-[clamp(...)]).
|
|
10
|
+
// Fluid sizes are named roles (text-display, text-section-title,
|
|
11
|
+
// text-lede); a genuinely new size becomes a token first.
|
|
12
|
+
//
|
|
13
|
+
// Both match string literals and template-literal chunks nested under any
|
|
14
|
+
// className attribute, so cn() and ternaries are covered. Non-className
|
|
15
|
+
// colour (JS colour maps, inline style objects) is deliberately not matched.
|
|
16
|
+
//
|
|
17
|
+
// import { designSystemGuardrails } from '@fracazo/design-system/eslint'
|
|
18
|
+
// export default defineConfig([
|
|
19
|
+
// ...,
|
|
20
|
+
// designSystemGuardrails({
|
|
21
|
+
// files: ['src/**/*.{ts,tsx}'],
|
|
22
|
+
// ignores: ['src/components/pdf/**'], // renderers that cannot use CSS vars
|
|
23
|
+
// }),
|
|
24
|
+
// ])
|
|
25
|
+
//
|
|
26
|
+
// A deliberate one-off carries `// eslint-disable-next-line
|
|
27
|
+
// no-restricted-syntax -- <reason>` so the exception is visible in review.
|
|
28
|
+
// =============================================================================
|
|
29
|
+
const COLOUR_REGEX = '(#[0-9a-fA-F]{3,8}|oklch\\(|rgba?\\(|hsla?\\()';
|
|
30
|
+
const COLOUR_MESSAGE = 'Arbitrary colour value in className. Use a semantic or primitive utility (bg-primary, text-muted-foreground, border-border, bg-surface) instead; see DESIGN.md. A deliberate one-off carries an eslint-disable-next-line stating why.';
|
|
31
|
+
const TYPE_CLAMP_REGEX = 'text-\\[clamp\\(';
|
|
32
|
+
const TYPE_CLAMP_MESSAGE = 'Arbitrary fluid type size in className. Use a named type role (text-display, text-section-title, text-lede) or add a token to roles.css; a deliberate one-off carries an eslint-disable-next-line stating why.';
|
|
33
|
+
const forClassName = (regex, message) => [
|
|
34
|
+
{ selector: `JSXAttribute[name.name='className'] Literal[value=/${regex}/]`, message },
|
|
35
|
+
{
|
|
36
|
+
selector: `JSXAttribute[name.name='className'] TemplateElement[value.cooked=/${regex}/]`,
|
|
37
|
+
message,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
export const noArbitraryColour = forClassName(COLOUR_REGEX, COLOUR_MESSAGE);
|
|
41
|
+
export const noArbitraryTypeClamp = forClassName(TYPE_CLAMP_REGEX, TYPE_CLAMP_MESSAGE);
|
|
42
|
+
/** A flat-config block: spread it into your eslint.config array. */
|
|
43
|
+
export function designSystemGuardrails({ files = ['src/**/*.{ts,tsx}'], ignores = [], } = {}) {
|
|
44
|
+
return {
|
|
45
|
+
files,
|
|
46
|
+
ignores,
|
|
47
|
+
rules: {
|
|
48
|
+
'no-restricted-syntax': ['error', ...noArbitraryColour, ...noArbitraryTypeClamp],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fracazo/design-system",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Roles, brand contract and guardrails for a warm, evidence-led product design system. Each product supplies a brand file; the system stays the same.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Alex Fracazo",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fracazo/design-system.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20.11"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"css",
|
|
17
|
+
"dist",
|
|
18
|
+
"demo",
|
|
19
|
+
"README.md",
|
|
20
|
+
"DESIGN.md"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./roles.css": "./css/roles.css",
|
|
24
|
+
"./eslint": {
|
|
25
|
+
"types": "./dist/guardrails/eslint.d.ts",
|
|
26
|
+
"default": "./dist/guardrails/eslint.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"ds-check-brand": "dist/guardrails/check-brand.js",
|
|
32
|
+
"ds-build-brand-css": "dist/guardrails/build-brand-css.js"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"tailwindcss": "^4"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22",
|
|
39
|
+
"typescript": "^5"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|