css-is-awesome 1.10.3 → 1.11.1
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/AGENTS.md +5 -3
- package/CHANGELOG.md +19 -0
- package/README.md +30 -6
- package/bin/analyze.cjs +170 -11
- package/css-is-awesome.instructions.md +9 -5
- package/dist/tokens.d.ts +1 -1
- package/llm.txt +3 -3
- package/package.json +1 -1
- package/public/theme.css +8 -0
- package/public/themes/press/theme.css +21 -0
- package/scripts/theme-contract.json +4 -0
- package/scss/_mixins.scss +52 -14
- package/scss/recipes/letterhead.md +156 -0
- package/scss/recipes/print-spec.md +14 -11
- package/scss/themes/press.scss +20 -0
package/AGENTS.md
CHANGED
|
@@ -298,7 +298,7 @@ Inside this package (all whitelisted in `files`):
|
|
|
298
298
|
- **Don't hand-edit `public/themes/**/theme.css` or `public/theme.css`.** They are build artifacts of `scss/themes/*.scss`. Edit the SCSS, run `npm run build:css:themes`, and `npm run check:theme-drift` to prove source and artifact agree.
|
|
299
299
|
- **Don't hand-write a theme's selector.** `@include cia.theme(name)` emits `:root, :root[data-theme="name"]` — both halves, on purpose. Writing `[data-theme=x]` yourself breaks the single-file drop-in; writing a bare `:root` yourself breaks the bundle.
|
|
300
300
|
- **Theme the numbered spacing scale, not the t-shirt aliases.** `--space-0`…`--space-9` are contract-required; `--space-md` and friends are optional `var()` aliases that follow them.
|
|
301
|
-
- **Print/PDF is a pure-CSS layer.** Include `cia.print-base` once at the stylesheet ROOT (it emits `@page`), then `cia.print-hidden` to drop chrome and `cia.print-only` to reveal paper-only content. Read `--is-print` (`0` screen / `1` paper) for custom effects. cia ships **zero JS** for it — the browser's native Print → Save as PDF is the generator. `print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch),
|
|
301
|
+
- **Print/PDF is a pure-CSS layer.** Include `cia.print-base` once at the stylesheet ROOT (it emits `@page`), then `cia.print-hidden` to drop chrome and `cia.print-only` to reveal paper-only content. Read `--is-print` (`0` screen / `1` paper) for custom effects. cia ships **zero JS** for it — the browser's native Print → Save as PDF is the generator. `print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch), defines a themeable print palette (`--print-ink/-paper/-line/-muted`, default ink-on-white) and rebinds the theme's colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it, so every theme — dark-only included — prints legible ink-on-paper; override a `--print-*` token to restyle paper (a letterhead), in a theme's own `@media print` or a `<link media="print">` sheet. Three opt-in flags, all default OFF: `$link-urls` + `$link-origin` (print each link's full destination via `attr(href)`), `$page-numbers` (sheet numbers in the `@page` footer). `$legible` is a deprecated no-op — the rebind supersedes it. Paginated spec-document output is the `print-spec` recipe.
|
|
302
302
|
- **Do NOT "clean up" the `!important` in the print mixins.** It is load-bearing and deliberate. `@media` contributes no specificity, so `print-hidden` carries only the specificity of the selector it is included in; a later equal-specificity `display` (usually a utility class or a component library) wins in print. Verified in a browser: with `!important` the element hides, without it it prints anyway — a silent, paper-only failure. **`@layer` does not fix this** — layered CSS always loses to unlayered CSS, so a layered print rule loses to any unlayered consumer stylesheet, and `!important` inverts layer order on top of that. cia is unlayered by decision (`.agent/decisions/decided/04-at-layer-decision.md`). Scope is 8 declarations, all inside `@media print`, all variable-driven via `--print-hide` / `--print-show`.
|
|
303
303
|
- **Don't ship JavaScript.** The npm package has zero `.js`/`.mjs` files. JS-dependent features ship as separate add-on packages.
|
|
304
304
|
- **Variants are arguments, not classes.** `cia.btn(primary)`, not `cia-btn cia-btn-primary` (Tier 1 utilities are an exception, but only at consumer level).
|
|
@@ -342,8 +342,10 @@ The SDK is an optional peer dep — `npm install -D @modelcontextprotocol/sdk zo
|
|
|
342
342
|
dumps a cia theme; `npx cia add <recipe>` (`--list` to browse) copies a recipe
|
|
343
343
|
from the book into the project — own the pattern; `npx cia analyze [path]`
|
|
344
344
|
audits a project's stylesheets against the installed API — dead `cia.*` symbols,
|
|
345
|
-
the `space()` 1–9 scale trap,
|
|
346
|
-
score and CI-ready exit codes.
|
|
345
|
+
the `space()` 1–9 scale trap, off-contract tokens (near-miss typos only), hard-coded
|
|
346
|
+
hex colors, BEM chains — with a health score and CI-ready exit codes. Low-noise on
|
|
347
|
+
color: a hex in a `var(--token, #hex)` fallback is token-driven, and a literal inside
|
|
348
|
+
`@media print` is an intentional paper colour — neither is flagged. Run any verb with `--help`. (`cia init` remains
|
|
347
349
|
planned.)
|
|
348
350
|
- **JSON token export** — DTCG-format token list in `figma-tokens/`.
|
|
349
351
|
- **`llm.txt`** — at the repo root and served from the docs site; single-fetch
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [1.11.1](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.11.0...v1.11.1) (2026-09-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **site:** second print example — a letter with letterhead + footer ([4891391](https://github.com/Jerry2d3d/css-is-awesome/commit/4891391bf29aaa0489473318e7ad0face04a772e))
|
|
7
|
+
|
|
8
|
+
# [1.11.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.10.3...v1.11.0) (2026-09-09)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **cli:** analyzer off-contract-token rule + print/var-aware color analysis ([6cd1eeb](https://github.com/Jerry2d3d/css-is-awesome/commit/6cd1eeb5feb082cc9c1bf5330d9c540b4cb332d7)), closes [hi#signal](https://github.com/hi/issues/signal) [#hex](https://github.com/Jerry2d3d/css-is-awesome/issues/hex)
|
|
14
|
+
* **cli:** graded, categorized analyzer report (EPIC-06 F3.1) ([ee56cdb](https://github.com/Jerry2d3d/css-is-awesome/commit/ee56cdbd17695f115226c4faeee6382a6041e7d5))
|
|
15
|
+
* **print:** letterhead recipe + Press newsprint print identity ([5963b0c](https://github.com/Jerry2d3d/css-is-awesome/commit/5963b0cce30eb1ce26d59230423477e41990747f))
|
|
16
|
+
* **print:** themeable print palette + token rebind; deprecate $legible ([bf4c657](https://github.com/Jerry2d3d/css-is-awesome/commit/bf4c65783ec22d03eed2b9836928151597828124))
|
|
17
|
+
* **site:** ACME letterhead invoice demo for the print theme ([5d5ed81](https://github.com/Jerry2d3d/css-is-awesome/commit/5d5ed81bf6f9c8490f2173f9d79b37f8f15fd7df))
|
|
18
|
+
* **site:** theme-editor print mode — paper-preview modal (EPIC-08 F5) ([1cfc83e](https://github.com/Jerry2d3d/css-is-awesome/commit/1cfc83e6c1a68eb39b144669802061710337c99c))
|
|
19
|
+
|
|
1
20
|
## [1.10.3](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.10.2...v1.10.3) (2026-09-07)
|
|
2
21
|
|
|
3
22
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Docs:** [cssisawesome.com](https://cssisawesome.com/) · **Install:** `npm install css-is-awesome`
|
|
10
10
|
|
|
11
|
-
> **The recipes book:** build any component in any framework using cia mixins —
|
|
11
|
+
> **The recipes book:** build any component in any framework using cia mixins — seven recipes today (`dialog`, `combobox`, `print-to-pdf`, `print-spec`, `letterhead`, `mobile-nav`, `bottom-nav`), with `datepicker`, `data-table` and `command-palette` queued. AI agents read recipes via MCP and generate components in your stack; humans read them at [`/docs/recipes`](https://cssisawesome.com/docs/recipes/).
|
|
12
12
|
|
|
13
13
|
## For AI agents — start here
|
|
14
14
|
|
|
@@ -202,7 +202,7 @@ Missing font files don't error, so a silent tofu box is the failure mode. If you
|
|
|
202
202
|
|
|
203
203
|
cia ships **no component library** — deliberately. Interactive patterns arrive as *recipes*: portable markdown files at [`scss/recipes/`](./scss/recipes/) that give you the correct HTML, the `cia.X` mixin calls to style it, and an a11y checklist graded against WCAG 2.2 AA. Copy the pattern into your own framework; you own the component, cia owns the styling and the accessibility homework.
|
|
204
204
|
|
|
205
|
-
**Shipped:** `dialog`, `combobox`, `print-to-pdf`, `print-spec`, `mobile-nav`, `bottom-nav`. Queued next: `datepicker`, `data-table`, `command-palette`.
|
|
205
|
+
**Shipped:** `dialog`, `combobox`, `print-to-pdf`, `print-spec`, `letterhead`, `mobile-nav`, `bottom-nav`. Queued next: `datepicker`, `data-table`, `command-palette`.
|
|
206
206
|
|
|
207
207
|
**Layout doctrine: Grid is the skeleton, Flex is the quick moves.** The page shell is CSS Grid with landmark-named areas (`nav / main / footer` — the map reads like the page); any control-dense region inside gets its own named-area grid whose `gap` carries all vertical rhythm; flex lives at the leaves for one-command flips (`cia.flex($direction: column)`). `cia.page-layout()` and `cia.layout()` own the maps — mobile is a different area map, never margin overrides.
|
|
208
208
|
|
|
@@ -227,10 +227,11 @@ The CLI also carries the registry and the health check:
|
|
|
227
227
|
npx cia add --list # browse the recipe book
|
|
228
228
|
npx cia add bottom-nav # copy a recipe into your project — you own the pattern
|
|
229
229
|
npx cia analyze src/styles # design-system health: dead cia.* symbols, the
|
|
230
|
-
# space() scale trap,
|
|
230
|
+
# space() scale trap, off-contract tokens (typos),
|
|
231
|
+
# hard-coded colors, BEM creep
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
`cia analyze` reads the real API surface from the installed package and exits non-zero on errors, so it slots straight into CI.
|
|
234
|
+
`cia analyze` reads the real API surface from the installed package and exits non-zero on errors, so it slots straight into CI. It's deliberately low-noise about color: a hex used as a `var(--token, #hex)` fallback is token-driven (not flagged), and a literal inside `@media print` is an intentional paper colour (print escapes theme colours by design). Off-contract-token findings only fire on a **near-miss** of a real token — a typo like `--inkk` — never on your own custom tokens. The default output is a **graded report** — a health score, a section per concern (Contract / Spacing / Color / Naming / Layout / API) with a `✓` when clean, and a suggested fix on each finding; add `--verbose` for the flat per-file list or `--json` for the machine shape.
|
|
234
235
|
|
|
235
236
|
## Print / PDF (zero JS)
|
|
236
237
|
|
|
@@ -241,7 +242,7 @@ Print support is a pure-CSS layer — the browser's native **Print → Save as P
|
|
|
241
242
|
|
|
242
243
|
// Once, in a GLOBAL stylesheet — never inside a component module.
|
|
243
244
|
// It emits its own :root block plus @page, so don't wrap it in a selector.
|
|
244
|
-
@include cia.print-base; // optional flags: ($size, $margin, $freeze-animations, $
|
|
245
|
+
@include cia.print-base; // optional flags: ($size, $margin, $freeze-animations, $link-urls, $link-origin, $page-numbers)
|
|
245
246
|
|
|
246
247
|
.site-nav { @include cia.print-hidden; } // drop chrome on paper
|
|
247
248
|
.print-note { @include cia.print-only; } // reveal paper-only content
|
|
@@ -250,7 +251,30 @@ Print support is a pure-CSS layer — the browser's native **Print → Save as P
|
|
|
250
251
|
|
|
251
252
|
`print-base` also collapses animations to zero duration and pins them to their final frame, so a page snapshotted mid-entrance-fade doesn't print as invisible text. It deliberately does **not** force `opacity: 1` or `transform: none` — that would fix the fade while flattening every intentional use of the same properties (a 0.15 watermark, a 0.4 disabled control, a stamp rotated `-4deg`). Elements that were never animating are left untouched. Read `--is-print` (`0` on screen, `1` on paper) for custom effects.
|
|
252
253
|
|
|
253
|
-
Inside `@media print`, `print-base` always forces `color-scheme: light`, so paired `light-dark()` themes print their light branch for free.
|
|
254
|
+
Inside `@media print`, `print-base` always forces `color-scheme: light`, so paired `light-dark()` themes print their light branch for free. It also defines a **print palette** — `--print-ink`, `--print-paper`, `--print-line`, `--print-muted` (default ink-on-white with light grays) — and rebinds the theme's own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it, so every theme prints as clean ink-on-paper and dark-only themes (Terminal) are legible with no flag. Three opt-in flags (all default off) take it further: `$link-urls` prints every link's destination via `attr(href)`; `$link-origin` prepends an origin so internal `/…` links resolve to full URLs on paper; `$page-numbers` numbers the sheets in the `@page` footer. (`$legible` is now a **deprecated no-op** — the token rebind supersedes it; passing it warns.) The docs site and the theme editor at `/themes` turn those flags on to print themselves as paginated spec documents.
|
|
255
|
+
|
|
256
|
+
### A print theme
|
|
257
|
+
|
|
258
|
+
Those four `--print-*` tokens are ordinary custom properties, so print is themeable like any other surface. The default every theme inherits is clean black-on-white with light grays; a theme (or a single project) opts into its own paper identity only if it wants one — override the tokens and every element re-inks, no component touched:
|
|
259
|
+
|
|
260
|
+
```scss
|
|
261
|
+
// A letterhead: navy ink, silver rules. Put this in a theme's own @media print
|
|
262
|
+
// block, or ship it as a print-only stylesheet loaded with <link media="print">.
|
|
263
|
+
@media print {
|
|
264
|
+
:root {
|
|
265
|
+
--print-ink: #16233f; // deep navy
|
|
266
|
+
--print-muted: #5b6b86;
|
|
267
|
+
--print-line: #c7ccd6; // silver hairlines
|
|
268
|
+
// --print-paper stays #fff
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Loading it as a paired print sheet is the same mechanism as paired light/dark themes:
|
|
274
|
+
|
|
275
|
+
```html
|
|
276
|
+
<link rel="stylesheet" href="/themes/letterhead/print.css" media="print">
|
|
277
|
+
```
|
|
254
278
|
|
|
255
279
|
**See it work.** Two live demonstrations back the walkthroughs. The [`print-spec`](./scss/recipes/print-spec.md) recipe paginates a page into a spec document (cover + table index, one page per section, honest sheet numbers). And [`/examples/print-to-pdf`](https://cssisawesome.com/examples/print-to-pdf/) is a real invoice you `Ctrl+P`: on paper the site chrome and the button vanish, the invoice fills the whole sheet, its links print as full followable URLs (`$link-urls` + `$link-origin`), a sheet number appears, and a **print-only QR code** — generated at build time as inline SVG, zero browser JS, no image fetch — links the paper back to the live [invoice-online](https://cssisawesome.com/examples/print-to-pdf/online) page. Full walkthroughs: the [`print-to-pdf`](./scss/recipes/print-to-pdf.md) and [`print-spec`](./scss/recipes/print-spec.md) recipes.
|
|
256
280
|
|
package/bin/analyze.cjs
CHANGED
|
@@ -19,6 +19,32 @@ const fs = require('fs');
|
|
|
19
19
|
const path = require('path');
|
|
20
20
|
|
|
21
21
|
const PKG_SCSS = path.join(__dirname, '..', 'scss');
|
|
22
|
+
const CONTRACT_PATH = path.join(__dirname, '..', 'scripts', 'theme-contract.json');
|
|
23
|
+
|
|
24
|
+
// All contract token names (required + optional). Used to catch typo'd tokens —
|
|
25
|
+
// a var(--x) that's a near-miss of a real token silently breaks the style.
|
|
26
|
+
// The contract is optional; if it can't be read the rule simply goes quiet.
|
|
27
|
+
function loadContractTokens() {
|
|
28
|
+
try {
|
|
29
|
+
const c = JSON.parse(fs.readFileSync(CONTRACT_PATH, 'utf8'));
|
|
30
|
+
return new Set([...(c.required || []), ...(c.optional || [])]);
|
|
31
|
+
} catch {
|
|
32
|
+
return new Set();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tiny Levenshtein (zero-dep). Only called on var(--x) misses, so cost is trivial.
|
|
37
|
+
function editDistance(a, b) {
|
|
38
|
+
const m = a.length, n = b.length;
|
|
39
|
+
const d = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
|
|
40
|
+
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
41
|
+
for (let i = 1; i <= m; i++) {
|
|
42
|
+
for (let j = 1; j <= n; j++) {
|
|
43
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return d[m][n];
|
|
47
|
+
}
|
|
22
48
|
|
|
23
49
|
const HELP = `cia analyze — design-system health check
|
|
24
50
|
|
|
@@ -33,6 +59,7 @@ Options:
|
|
|
33
59
|
Auto-detected per file from @use lines; use this when
|
|
34
60
|
imports are aliased through an intermediate file.
|
|
35
61
|
--json Machine-readable report on stdout.
|
|
62
|
+
--verbose The per-file listing instead of the graded report.
|
|
36
63
|
--strict Exit 1 on warnings too (default: errors only).
|
|
37
64
|
|
|
38
65
|
Examples:
|
|
@@ -106,7 +133,47 @@ function stripComments(src) {
|
|
|
106
133
|
return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
|
|
136
|
+
// A hex inside a `var(--token, #hex)` fallback is token-driven, not a hard-coded
|
|
137
|
+
// color — that's the correct pattern (token first, literal only if unset).
|
|
138
|
+
// Strip var() expressions innermost-first so nested fallbacks
|
|
139
|
+
// (`var(--a, var(--b, #fff))`) collapse too, leaving only genuine literals.
|
|
140
|
+
function stripVarExpr(src) {
|
|
141
|
+
let prev;
|
|
142
|
+
let s = src;
|
|
143
|
+
do {
|
|
144
|
+
prev = s;
|
|
145
|
+
s = s.replace(/var\([^()]*\)/g, '');
|
|
146
|
+
} while (s !== prev);
|
|
147
|
+
return s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Literals inside an @media print block are intentional: print deliberately
|
|
151
|
+
// escapes theme colours (ink-on-white for legibility, grays for rules) so a
|
|
152
|
+
// hard-coded #000/#fff there is correct, not a smell. Remove print @media
|
|
153
|
+
// blocks — brace-balanced, so nested selectors are handled — before the
|
|
154
|
+
// hard-coded-color scan. Non-print @media queries are kept and still scanned.
|
|
155
|
+
function stripPrintBlocks(src) {
|
|
156
|
+
let out = '';
|
|
157
|
+
let i = 0;
|
|
158
|
+
while (i < src.length) {
|
|
159
|
+
const at = src.indexOf('@media', i);
|
|
160
|
+
if (at === -1) { out += src.slice(i); break; }
|
|
161
|
+
const braceOpen = src.indexOf('{', at);
|
|
162
|
+
if (braceOpen === -1) { out += src.slice(i); break; }
|
|
163
|
+
out += src.slice(i, at);
|
|
164
|
+
let depth = 0;
|
|
165
|
+
let j = braceOpen;
|
|
166
|
+
for (; j < src.length; j++) {
|
|
167
|
+
if (src[j] === '{') depth++;
|
|
168
|
+
else if (src[j] === '}' && --depth === 0) { j++; break; }
|
|
169
|
+
}
|
|
170
|
+
if (!/\bprint\b/.test(src.slice(at + 6, braceOpen))) out += src.slice(at, j);
|
|
171
|
+
i = j;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function analyzeFile(file, symbols, contractTokens, extraNs) {
|
|
110
177
|
const raw = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
111
178
|
const src = stripComments(raw);
|
|
112
179
|
const ns = detectNamespaces(src, extraNs);
|
|
@@ -130,7 +197,29 @@ function analyzeFile(file, symbols, extraNs) {
|
|
|
130
197
|
}
|
|
131
198
|
}
|
|
132
199
|
}
|
|
133
|
-
|
|
200
|
+
// off-contract-token: a var(--x) that's a near-miss of a real contract token
|
|
201
|
+
// (a typo → the declaration silently fails). Pure custom tokens — not close to
|
|
202
|
+
// any contract token — are the consumer's own and are left alone.
|
|
203
|
+
if (contractTokens && contractTokens.size) {
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const m of src.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
206
|
+
const tok = m[1];
|
|
207
|
+
if (contractTokens.has(tok) || seen.has(tok)) continue;
|
|
208
|
+
seen.add(tok);
|
|
209
|
+
let best = null;
|
|
210
|
+
let bestD = Infinity;
|
|
211
|
+
for (const known of contractTokens) {
|
|
212
|
+
const d = editDistance(tok, known);
|
|
213
|
+
if (d < bestD) { bestD = d; best = known; }
|
|
214
|
+
}
|
|
215
|
+
if (best && bestD > 0 && bestD <= 2) {
|
|
216
|
+
findings.push({ level: 'warn', rule: 'off-contract-token', detail: `${tok} — not a contract token; did you mean ${best}?` });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Genuine literal colors only — a hex used as a var() fallback is token-driven,
|
|
221
|
+
// and a hex inside @media print is an intentional paper colour.
|
|
222
|
+
for (const m of stripVarExpr(stripPrintBlocks(src)).matchAll(/#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
|
|
134
223
|
findings.push({ level: 'warn', rule: 'hard-coded-color', detail: `${m[0]} — values should come from tokens (cia.color(...) / var(--...))` });
|
|
135
224
|
}
|
|
136
225
|
for (const m of src.matchAll(/\.[a-zA-Z][\w]*(?:__|--)[\w-]+/g)) {
|
|
@@ -144,6 +233,31 @@ function analyzeFile(file, symbols, extraNs) {
|
|
|
144
233
|
|
|
145
234
|
// ── report ──────────────────────────────────────────────────────────────────
|
|
146
235
|
|
|
236
|
+
// Every rule belongs to a category, so the default report reads as a graded
|
|
237
|
+
// health check (a section per concern) rather than a flat dump. Categories with
|
|
238
|
+
// no findings print a ✓.
|
|
239
|
+
const RULE_CATEGORY = {
|
|
240
|
+
'unknown-symbol': 'API',
|
|
241
|
+
'space-scale': 'Spacing',
|
|
242
|
+
'off-contract-token': 'Contract',
|
|
243
|
+
'hard-coded-color': 'Color',
|
|
244
|
+
bem: 'Naming',
|
|
245
|
+
'hand-written-areas': 'Layout',
|
|
246
|
+
};
|
|
247
|
+
// The categories with rules implemented today — shown ✓ when clean so a passing
|
|
248
|
+
// audit reads as coverage, not silence.
|
|
249
|
+
const IMPLEMENTED_CATEGORIES = ['API', 'Contract', 'Spacing', 'Color', 'Naming', 'Layout'];
|
|
250
|
+
|
|
251
|
+
// Details are authored as "claim — suggested fix"; split so the graded report
|
|
252
|
+
// can put the fix on its own `→` line.
|
|
253
|
+
function splitDetail(detail) {
|
|
254
|
+
const i = detail.indexOf(' — ');
|
|
255
|
+
if (i === -1) return { claim: detail, suggestion: '' };
|
|
256
|
+
return { claim: detail.slice(0, i), suggestion: detail.slice(i + 3) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const LEVEL_MARK = { error: '✗', warn: '⚠', info: 'ℹ' };
|
|
260
|
+
|
|
147
261
|
async function run(args) {
|
|
148
262
|
if (args[0] === '-h' || args[0] === '--help' || args[0] === 'help') {
|
|
149
263
|
process.stdout.write(HELP);
|
|
@@ -151,6 +265,7 @@ async function run(args) {
|
|
|
151
265
|
}
|
|
152
266
|
const json = args.includes('--json');
|
|
153
267
|
const strict = args.includes('--strict');
|
|
268
|
+
const verbose = args.includes('--verbose');
|
|
154
269
|
const nsFlag = args.indexOf('--namespace');
|
|
155
270
|
const extraNs = nsFlag !== -1 && args[nsFlag + 1] ? args[nsFlag + 1].split(',') : [];
|
|
156
271
|
const target = path.resolve(args.find((a) => !a.startsWith('--') && a !== extraNs.join(',')) || '.');
|
|
@@ -161,28 +276,72 @@ async function run(args) {
|
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
const symbols = collectSymbols();
|
|
279
|
+
const contractTokens = loadContractTokens();
|
|
164
280
|
const files = walkScss(target);
|
|
165
|
-
const results = files.map((f) => analyzeFile(f, symbols, extraNs)).filter((r) => r.findings.length || r.namespaces.length);
|
|
281
|
+
const results = files.map((f) => analyzeFile(f, symbols, contractTokens, extraNs)).filter((r) => r.findings.length || r.namespaces.length);
|
|
166
282
|
|
|
167
283
|
const counts = { error: 0, warn: 0, info: 0 };
|
|
168
284
|
for (const r of results) for (const f of r.findings) counts[f.level]++;
|
|
169
285
|
const ciaFiles = results.filter((r) => r.namespaces.length).length;
|
|
170
286
|
const health = Math.max(0, 100 - counts.error * 10 - counts.warn * 2 - counts.info);
|
|
171
287
|
|
|
288
|
+
// Enrich every finding with its category and a split-out suggestion — additive
|
|
289
|
+
// fields, so `--json` consumers of counts/health/results keep working.
|
|
290
|
+
for (const r of results) {
|
|
291
|
+
for (const f of r.findings) {
|
|
292
|
+
f.category = RULE_CATEGORY[f.rule] || 'Other';
|
|
293
|
+
const { suggestion } = splitDetail(f.detail);
|
|
294
|
+
if (suggestion) f.suggestion = suggestion;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rel = path.relative(process.cwd(), target) || '.';
|
|
299
|
+
const out = (s) => process.stdout.write(s);
|
|
300
|
+
|
|
172
301
|
if (json) {
|
|
173
|
-
|
|
174
|
-
} else {
|
|
175
|
-
|
|
176
|
-
|
|
302
|
+
out(JSON.stringify({ target, files: files.length, ciaFiles, apiSymbols: symbols.size, counts, health, results }, null, 2) + '\n');
|
|
303
|
+
} else if (verbose) {
|
|
304
|
+
// The per-file listing — every finding under its file, unabridged.
|
|
305
|
+
out(`\ncia analyze — ${rel}\n`);
|
|
306
|
+
out(`${files.length} scss file(s), ${ciaFiles} using cia, ${symbols.size} API symbols known\n\n`);
|
|
177
307
|
for (const r of results) {
|
|
178
308
|
if (!r.findings.length) continue;
|
|
179
|
-
|
|
309
|
+
out(`${path.relative(process.cwd(), r.file)}\n`);
|
|
180
310
|
for (const f of r.findings) {
|
|
181
|
-
|
|
182
|
-
process.stdout.write(` ${mark} [${f.rule}] ${f.detail}\n`);
|
|
311
|
+
out(` ${LEVEL_MARK[f.level]} [${f.rule}] ${f.detail}\n`);
|
|
183
312
|
}
|
|
184
313
|
}
|
|
185
|
-
|
|
314
|
+
out(`\nDesign-system health: ${health}% (${counts.error} error, ${counts.warn} warn, ${counts.info} info)\n`);
|
|
315
|
+
} else {
|
|
316
|
+
// The graded report — a section per category, ✓ when clean, each finding
|
|
317
|
+
// with its file and a suggested fix on its own line.
|
|
318
|
+
const all = [];
|
|
319
|
+
for (const r of results) {
|
|
320
|
+
for (const f of r.findings) all.push({ ...f, file: path.relative(process.cwd(), r.file) });
|
|
321
|
+
}
|
|
322
|
+
const extraCats = [...new Set(all.map((f) => f.category))].filter((c) => !IMPLEMENTED_CATEGORIES.includes(c));
|
|
323
|
+
const cats = [...IMPLEMENTED_CATEGORIES, ...extraCats];
|
|
324
|
+
|
|
325
|
+
out(`\ncia analyze — ${rel}\n`);
|
|
326
|
+
out(`${files.length} scss file(s) · ${ciaFiles} using cia · ${symbols.size} API symbols\n\n`);
|
|
327
|
+
out(`Design-system health: ${health}/100\n\n`);
|
|
328
|
+
|
|
329
|
+
for (const cat of cats) {
|
|
330
|
+
const items = all.filter((f) => f.category === cat);
|
|
331
|
+
if (!items.length) {
|
|
332
|
+
out(` ${cat.padEnd(9)} ✓\n`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const worst = items.some((i) => i.level === 'error') ? 'error' : items.some((i) => i.level === 'warn') ? 'warn' : 'info';
|
|
336
|
+
out(` ${cat.padEnd(9)} ${LEVEL_MARK[worst]} ${items.length}\n`);
|
|
337
|
+
for (const it of items) {
|
|
338
|
+
out(` ${LEVEL_MARK[it.level]} ${it.file} ${it.suggestion ? it.claim ?? splitDetail(it.detail).claim : it.detail}\n`);
|
|
339
|
+
if (it.suggestion) out(` → ${it.suggestion}\n`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const tally = `${counts.error} error · ${counts.warn} warn · ${counts.info} info`;
|
|
344
|
+
out(`\n${tally}${counts.error || counts.warn || counts.info ? ' · run with --verbose for the per-file list' : ''}\n`);
|
|
186
345
|
}
|
|
187
346
|
|
|
188
347
|
if (counts.error > 0 || (strict && counts.warn > 0)) process.exit(1);
|
|
@@ -176,13 +176,17 @@ browser's native Print → Save as PDF is the generator; cia just supplies the
|
|
|
176
176
|
ROOT** — it emits `@page` (invalid when nested in a selector), freezes
|
|
177
177
|
animations so nothing prints invisible, and emits the print variable
|
|
178
178
|
control plane. In `@media print` it always forces `color-scheme: light`, so
|
|
179
|
-
paired `light-dark()` themes print their light branch for free.
|
|
180
|
-
|
|
181
|
-
`--muted
|
|
182
|
-
|
|
179
|
+
paired `light-dark()` themes print their light branch for free. It also
|
|
180
|
+
defines a **print palette** — `--print-ink`, `--print-paper`, `--print-line`,
|
|
181
|
+
`--print-muted` (default ink-on-white with grays) — and rebinds the theme's
|
|
182
|
+
own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it,
|
|
183
|
+
so every theme, dark-only (Terminal) included, prints legible ink-on-paper;
|
|
184
|
+
override a `--print-*` token to restyle paper (a letterhead). Three opt-in
|
|
185
|
+
flags (all default OFF): `$link-urls` prints each link's destination via
|
|
183
186
|
`attr(href)`; `$link-origin` prepends an origin to internal `/…` links so they
|
|
184
187
|
resolve to full URLs; `$page-numbers` puts `counter(page)` in the `@page`
|
|
185
|
-
bottom-center margin box.
|
|
188
|
+
bottom-center margin box. `$legible` is retained as a **deprecated no-op** —
|
|
189
|
+
the rebind supersedes it and passing it warns.
|
|
186
190
|
- **`print-hidden`** — hide an element on paper (the "hide the nav" case).
|
|
187
191
|
- **`print-only`** — show an element only on paper (e.g. an inline URL
|
|
188
192
|
footer); hidden on screen.
|
package/dist/tokens.d.ts
CHANGED
package/llm.txt
CHANGED
|
@@ -45,10 +45,10 @@ silent.
|
|
|
45
45
|
|
|
46
46
|
## What cia provides (one-line each)
|
|
47
47
|
|
|
48
|
-
- **Mixins** — `cia.btn`, `cia.card`, `cia.accordion`, `cia.modal`, `cia.tooltip`, `cia.dropdown`, `cia.tabs`, `cia.copy-button`, `cia.hamburger`, `cia.drawer`, `cia.sheet`, `cia.dock`, `cia.stepper`, `cia.progress`, `cia.wizard-shell`, `cia.sidebar`, `cia.toolbar`, `cia.stack`, `cia.cluster`, `cia.switcher`, `cia.cover`, `cia.frame`, `cia.media`, `cia.contain`, `cia.focus-ring`, `cia.sr-only`, `cia.print`, `cia.print-base`, `cia.print-hidden`, `cia.print-only`, `cia.color`, `cia.space`, `cia.radius`, `cia.shadow`, `cia.font`, `cia.transition`, `cia.animate`, plus many more. **`cia.print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch)
|
|
48
|
+
- **Mixins** — `cia.btn`, `cia.card`, `cia.accordion`, `cia.modal`, `cia.tooltip`, `cia.dropdown`, `cia.tabs`, `cia.copy-button`, `cia.hamburger`, `cia.drawer`, `cia.sheet`, `cia.dock`, `cia.stepper`, `cia.progress`, `cia.wizard-shell`, `cia.sidebar`, `cia.toolbar`, `cia.stack`, `cia.cluster`, `cia.switcher`, `cia.cover`, `cia.frame`, `cia.media`, `cia.contain`, `cia.focus-ring`, `cia.sr-only`, `cia.print`, `cia.print-base`, `cia.print-hidden`, `cia.print-only`, `cia.color`, `cia.space`, `cia.radius`, `cia.shadow`, `cia.font`, `cia.transition`, `cia.animate`, plus many more. **`cia.print-base` always forces `color-scheme: light` in print (paired `light-dark()` themes print their light branch), defines a themeable print palette (`--print-ink` / `--print-paper` / `--print-line` / `--print-muted`, default ink-on-white with grays) and rebinds the theme's own colour tokens (`--ink`, `--surface-*`, `--border-*`, `--code-*`) onto it — so every theme, dark-only (Terminal) included, prints legible ink-on-paper, and overriding a `--print-*` token restyles paper (a letterhead) without touching a component. Three opt-in flags, all default OFF:** `$link-urls` + `$link-origin` (print each link's full destination via `attr(href)`), `$page-numbers` (sheet numbers in the `@page` footer). `$legible` is a DEPRECATED no-op — the token rebind supersedes it and passing it warns.
|
|
49
49
|
- **24 themes / 8 families** — boilerplate, sketchbook, press, prism, cupertino, glass, graphite, terminal. Each family ships three files: an unsuffixed dual-mode base (both modes via `light-dark()`) plus pinned `-light` and `-dark` single-mode variants for `<link media>` pairing. Every one has exactly one SCSS source under `scss/themes/`; `public/themes/<name>/theme.css` is BUILD OUTPUT and is gated against its source by `npm run check:theme-drift` — never hand-edit it. MCP `list_themes` returns **24**. Say **24 themes across 8 families** when you need one number.
|
|
50
50
|
- **6 zero-JS interactive components** — accordion (`<details name>`), modal (`<dialog>`), tooltip (`popover="hint"`), dropdown (`[popover]` — the mixin re-asserts the UA's closed state and restores `display: flex` only under `:popover-open`, so menus never render permanently open on popover markup), tabs (radio + `:has()`), copy-button (Clipboard API via consumer-wired JS).
|
|
51
|
-
- **CLI** (`npx cia`) — `migrate tailwind|bootstrap` (config → cia theme), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, hard-coded hex, BEM; health score, CI exit codes).
|
|
51
|
+
- **CLI** (`npx cia`) — `migrate tailwind|bootstrap` (config → cia theme), `add <recipe>` (copy a recipe from the book into the project), `analyze [path]` (audit stylesheets against the installed API: dead `cia.*` symbols, the `space()` 1–9 trap, off-contract tokens (near-miss typos of real tokens only, never your own custom tokens), hard-coded hex, BEM; health score, CI exit codes). Low-noise on color: a hex in a `var(--token, #hex)` fallback is token-driven and a literal inside `@media print` is an intentional paper colour — neither is flagged.
|
|
52
52
|
- **Mobile navigation family** — `cia.hamburger` / `cia.drawer` / `cia.sheet` / `cia.dock` ride `[popover]` + CSS Grid, zero JS; recipes `mobile-nav` (hamburger + drawer) and `bottom-nav` (dock + sheets). House rule: **on phones things take the space they're in** — a `cia.dropdown` menu opens 1px under its full-width trigger at the trigger's exact width via CSS anchor positioning (`position-try-fallbacks: flip-block` flips it above at the screen bottom; set `width: auto` at `&[popover]` specificity or the UA's `[popover] { width: fit-content }` and the mixin's inset reset win). Full spec: AGENTS.md quick decision #8 and `/docs/mobile`.
|
|
53
53
|
- **163 contract tokens — 127 required + 36 optional** — surfaces, ink, lines, colors, type, radius, shadow, blur, glow, motion, z-index, spacing, semantic aliases.
|
|
54
54
|
- **Spacing is themeable, and the numbered scale is the knob.** Themes declare `--space-0` … `--space-9` (contract-required). The t-shirt names (`--space-md`, `--space-lg` …) are contract-OPTIONAL and emitted by the library as `var()` aliases onto the numbered steps. `space(4)` compiles to `var(--space-4)`, so **theme the numbered step, never the alias** — setting `--space-md` alone leaves every component untouched. Library defaults emit under `:where(:root)` (specificity 0,0,0) so any theme declaration outranks them regardless of load order.
|
|
@@ -170,7 +170,7 @@ cia ships a **recipes book**: portable patterns for building accessible componen
|
|
|
170
170
|
|
|
171
171
|
AI agents read recipes via MCP `list_recipes` / `get_recipe(name)` and generate consumer components in any framework. Humans read them at `/docs/recipes` and copy patterns directly.
|
|
172
172
|
|
|
173
|
-
**Shipped today —
|
|
173
|
+
**Shipped today — 7 recipes: dialog, combobox, print-to-pdf, print-spec, letterhead, mobile-nav, bottom-nav.** Queued next: datepicker, data-table, command-palette. **No component library** — recipes are the deliverable. See `scss/recipes/README.md` for the schema.
|
|
174
174
|
|
|
175
175
|
## Priority ladder (the v1.0 pitch order)
|
|
176
176
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "css-is-awesome",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "A token-driven SCSS design system with light/dark theming, semantic color tokens, and a 800+ LOC mixin API.",
|
|
5
5
|
"homepage": "https://github.com/Jerry2d3d/css-is-awesome#readme",
|
|
6
6
|
"bugs": {
|
package/public/theme.css
CHANGED
|
@@ -2405,6 +2405,14 @@
|
|
|
2405
2405
|
--card-border: 1px solid var(--hair); /* thin rule instead */
|
|
2406
2406
|
--input-radius: var(--r-sm); /* 2px */
|
|
2407
2407
|
--modal-radius: var(--r-md); /* 3px */
|
|
2408
|
+
/* ----------------------------------------------------------------
|
|
2409
|
+
PRINT — Press's own paper identity (opt-in per-theme override).
|
|
2410
|
+
Sass hoists this @media out of the theme selector, so it emits as
|
|
2411
|
+
`@media print { :root, :root[data-theme="press"] { … } }` and
|
|
2412
|
+
overrides print-base's default palette on paper only. Newsprint:
|
|
2413
|
+
a faint warm stock, warm near-black ink, press-red masthead rules.
|
|
2414
|
+
Every other theme inherits print-base's plain ink-on-white.
|
|
2415
|
+
---------------------------------------------------------------- */
|
|
2408
2416
|
}
|
|
2409
2417
|
:root[data-theme=press-dark] {
|
|
2410
2418
|
color-scheme: dark;
|
|
@@ -196,4 +196,25 @@
|
|
|
196
196
|
--card-border: 1px solid var(--hair); /* thin rule instead */
|
|
197
197
|
--input-radius: var(--r-sm); /* 2px */
|
|
198
198
|
--modal-radius: var(--r-md); /* 3px */
|
|
199
|
+
/* ----------------------------------------------------------------
|
|
200
|
+
PRINT — Press's own paper identity (opt-in per-theme override).
|
|
201
|
+
Sass hoists this @media out of the theme selector, so it emits as
|
|
202
|
+
`@media print { :root, :root[data-theme="press"] { … } }` and
|
|
203
|
+
overrides print-base's default palette on paper only. Newsprint:
|
|
204
|
+
a faint warm stock, warm near-black ink, press-red masthead rules.
|
|
205
|
+
Every other theme inherits print-base's plain ink-on-white.
|
|
206
|
+
---------------------------------------------------------------- */
|
|
207
|
+
}
|
|
208
|
+
@media print {
|
|
209
|
+
:root, :root[data-theme=press] {
|
|
210
|
+
/* stylelint-disable no-invalid-position-declaration -- these are custom-property
|
|
211
|
+
overrides scoped to the theme selector m.theme() supplies; the position is valid
|
|
212
|
+
once the mixin wraps them (emits `@media print { :root, :root[data-theme=press] }`),
|
|
213
|
+
but stylelint can't see the mixin's parent selector statically. */
|
|
214
|
+
--print-paper: #FBFAF6; /* faint warm newsprint stock */
|
|
215
|
+
--print-ink: #0A0A0A; /* warm near-black */
|
|
216
|
+
--print-line: #D93025; /* press red — the masthead rule */
|
|
217
|
+
--print-muted: #6B6963; /* warm editorial gray */
|
|
218
|
+
/* stylelint-enable no-invalid-position-declaration */
|
|
219
|
+
}
|
|
199
220
|
}
|
package/scss/_mixins.scss
CHANGED
|
@@ -598,6 +598,14 @@ $_type-scale: (
|
|
|
598
598
|
// Override either locally for a per-element exception — no rule rewrite:
|
|
599
599
|
// .keep-this { --print-hide: revert; } // stays on paper despite print-hidden
|
|
600
600
|
// .pdf-row { --print-show: flex; } // print-only block laid out as flex
|
|
601
|
+
//
|
|
602
|
+
// Also emits a themeable PRINT PALETTE — --print-ink / --print-paper /
|
|
603
|
+
// --print-line / --print-muted (default ink-on-white) — and rebinds the theme's
|
|
604
|
+
// colour tokens (--ink, --surface-*, --border-*, --code-*) onto it inside
|
|
605
|
+
// @media print, so every theme (dark-only included) prints legible ink-on-paper.
|
|
606
|
+
// Override a --print-* token (in a theme's @media print, or a <link media=print>
|
|
607
|
+
// sheet) to restyle paper. $legible is a deprecated no-op — the rebind supersedes
|
|
608
|
+
// it and passing it warns.
|
|
601
609
|
@mixin print-base(
|
|
602
610
|
$freeze-animations: true,
|
|
603
611
|
$size: letter,
|
|
@@ -607,6 +615,10 @@ $_type-scale: (
|
|
|
607
615
|
$link-origin: null,
|
|
608
616
|
$page-numbers: false
|
|
609
617
|
) {
|
|
618
|
+
@if $legible {
|
|
619
|
+
@warn "print-base: $legible is deprecated and now a no-op — print rebinds --ink onto --print-ink for every theme, so dark-only themes are legible without it. Remove the flag; override --print-* tokens to restyle paper.";
|
|
620
|
+
}
|
|
621
|
+
|
|
610
622
|
:root {
|
|
611
623
|
--is-print: 0;
|
|
612
624
|
--print-hide: none;
|
|
@@ -619,22 +631,48 @@ $_type-scale: (
|
|
|
619
631
|
|
|
620
632
|
// Paired themes build color as light-dark(): forcing the light scheme
|
|
621
633
|
// makes them print their light branch — light text becomes dark text
|
|
622
|
-
// on the (already white) sheet, for free.
|
|
623
|
-
// values and ignore this; $legible below covers them.
|
|
634
|
+
// on the (already white) sheet, for free.
|
|
624
635
|
color-scheme: light;
|
|
625
636
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
637
|
+
// Print palette — themeable. Override these (in a theme's own @media
|
|
638
|
+
// print, or a <link media="print"> theme file) to restyle paper: navy
|
|
639
|
+
// ink, a letterhead rule, a grayscale draft. Default: ink on white.
|
|
640
|
+
--print-ink: #000;
|
|
641
|
+
--print-paper: #fff;
|
|
642
|
+
--print-line: #999;
|
|
643
|
+
--print-muted: #666;
|
|
644
|
+
|
|
645
|
+
// Rebind the theme's colour tokens onto the print palette, so every
|
|
646
|
+
// element that reads --ink / --surface / --border prints ink-on-paper
|
|
647
|
+
// and re-inks when a --print-* token is overridden. Supersedes the old
|
|
648
|
+
// $legible remap: dark-only themes (Terminal) are legible with no flag.
|
|
649
|
+
--ink: var(--print-ink);
|
|
650
|
+
--ink-soft: var(--print-muted);
|
|
651
|
+
--ink-faint: var(--print-muted);
|
|
652
|
+
--muted: var(--print-muted);
|
|
653
|
+
--text-primary: var(--print-ink);
|
|
654
|
+
--text-secondary: var(--print-muted);
|
|
655
|
+
--text-tertiary: var(--print-muted);
|
|
656
|
+
|
|
657
|
+
--paper: var(--print-paper);
|
|
658
|
+
--paper-raised: var(--print-paper);
|
|
659
|
+
--paper-sunk: var(--print-paper);
|
|
660
|
+
--surface-default: var(--print-paper);
|
|
661
|
+
--surface-raised: var(--print-paper);
|
|
662
|
+
--surface-sunk: var(--print-paper);
|
|
663
|
+
--background-default: var(--print-paper);
|
|
664
|
+
|
|
665
|
+
--border-default: var(--print-line);
|
|
666
|
+
--hair: var(--print-line);
|
|
667
|
+
|
|
668
|
+
--code-bg: var(--print-paper);
|
|
669
|
+
--code-ink: var(--print-ink);
|
|
670
|
+
--code-muted: var(--print-muted);
|
|
636
671
|
}
|
|
637
672
|
|
|
673
|
+
// Paper floor: ink on white regardless of what the app sets on <body>.
|
|
674
|
+
body { background: var(--print-paper); color: var(--print-ink); }
|
|
675
|
+
|
|
638
676
|
@page { size: $size; margin: $margin; }
|
|
639
677
|
|
|
640
678
|
@if $page-numbers {
|
|
@@ -651,14 +689,14 @@ $_type-scale: (
|
|
|
651
689
|
a[href^="http"]::after {
|
|
652
690
|
content: " (" attr(href) ")";
|
|
653
691
|
font-size: 0.85em;
|
|
654
|
-
color:
|
|
692
|
+
color: var(--print-muted);
|
|
655
693
|
word-break: break-all;
|
|
656
694
|
}
|
|
657
695
|
@if $link-origin {
|
|
658
696
|
a[href^="/"]::after {
|
|
659
697
|
content: " (#{$link-origin}" attr(href) ")";
|
|
660
698
|
font-size: 0.85em;
|
|
661
|
-
color:
|
|
699
|
+
color: var(--print-muted);
|
|
662
700
|
word-break: break-all;
|
|
663
701
|
}
|
|
664
702
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: letterhead
|
|
3
|
+
description: A print-only letterhead — a branded header (name, address, a rule) that appears only when the page is printed, styled with cia's print-only mixin and the themeable --print-* palette.
|
|
4
|
+
category: layout
|
|
5
|
+
complexity: simple
|
|
6
|
+
cia-version: ">=1.11.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Use this when
|
|
10
|
+
|
|
11
|
+
You have a page — an invoice, a quote, a report — that people **print**, and
|
|
12
|
+
you want a branded header on the paper that isn't on the screen: a company
|
|
13
|
+
name, an address line, a rule under it. A letterhead. It's not a theme and it's
|
|
14
|
+
not a colour scheme — it's a small piece of **markup you add**, styled with
|
|
15
|
+
cia's print helpers so it shows on paper only and matches the print palette.
|
|
16
|
+
|
|
17
|
+
Pair it with [`print-to-pdf`](./print-to-pdf.md) (the page *is* the document) or
|
|
18
|
+
[`print-spec`](./print-spec.md) (a paginated spec document).
|
|
19
|
+
|
|
20
|
+
## Structure (raw HTML)
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<article class="doc">
|
|
24
|
+
<!-- Only rendered on paper. On screen your site chrome carries identity. -->
|
|
25
|
+
<header class="letterhead">
|
|
26
|
+
<p class="letterhead-name">ACME CORPORATION</p>
|
|
27
|
+
<p class="letterhead-tag">Threat mitigation since 1949</p>
|
|
28
|
+
<p class="letterhead-addr">1 Cliffside Drive · Painted Desert Mesa · acme.example</p>
|
|
29
|
+
</header>
|
|
30
|
+
|
|
31
|
+
<!-- …the rest of the document… -->
|
|
32
|
+
</article>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Styling (cia mixins)
|
|
36
|
+
|
|
37
|
+
`cia.print-only` hides the block on screen and reveals it in `@media print`. It
|
|
38
|
+
reads the **print palette** — `--print-ink`, `--print-line`, `--print-muted` —
|
|
39
|
+
that `cia.print-base` defines, so the letterhead always matches the paper look
|
|
40
|
+
and follows any per-theme print override (a newsprint red rule, a navy ink).
|
|
41
|
+
|
|
42
|
+
```scss
|
|
43
|
+
@use 'css-is-awesome/api' as cia;
|
|
44
|
+
|
|
45
|
+
// Requires cia.print-base once at the stylesheet ROOT (it defines --print-*).
|
|
46
|
+
.letterhead {
|
|
47
|
+
@include cia.print-only;
|
|
48
|
+
margin-block-end: 1rem;
|
|
49
|
+
padding-block-end: 0.5rem;
|
|
50
|
+
border-block-end: 3px double var(--print-line); // the paper's rule colour
|
|
51
|
+
text-align: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.letterhead-name {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-family: var(--font-display);
|
|
57
|
+
font-size: 1.9rem;
|
|
58
|
+
letter-spacing: 0.14em;
|
|
59
|
+
color: var(--print-ink); // the paper's ink
|
|
60
|
+
}
|
|
61
|
+
.letterhead-tag { margin: 0.15rem 0 0; font-style: italic; color: var(--print-muted); }
|
|
62
|
+
.letterhead-addr { margin: 0.35rem 0 0; font-family: var(--font-mono); font-size: 0.62rem; color: var(--print-muted); }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Read `--print-*` directly rather than `--ink` / `--border-default`: in print
|
|
66
|
+
those are already rebound onto the palette, but naming the print tokens states
|
|
67
|
+
the intent and survives a per-theme print override unchanged.
|
|
68
|
+
|
|
69
|
+
## Interactivity
|
|
70
|
+
|
|
71
|
+
**Zero JS.** A letterhead is static markup — there is nothing to wire. It is
|
|
72
|
+
absent from the screen (`print-only` sets `display: none`) and present on paper
|
|
73
|
+
because `@media print` reveals it. No script, no state, no server.
|
|
74
|
+
|
|
75
|
+
## A11y checklist
|
|
76
|
+
|
|
77
|
+
- [ ] The letterhead is branding, not content — never put information the reader needs *only* here; it is absent from the screen, so the on-screen header must carry the real identity and links ([WCAG 2.2 SC 1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html))
|
|
78
|
+
- [ ] Keep the wordmark a `<p>`, not a heading, so a print-only `<h1>` doesn't fork the document outline between screen and paper ([WCAG 2.2 SC 2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html))
|
|
79
|
+
- [ ] A logo `<img>` carries a real `alt` (the company name), or `alt=""` when adjacent text already names it ([WCAG 2.2 SC 1.1.1 Non-text Content](https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html))
|
|
80
|
+
- [ ] Ink-on-paper contrast meets the minimum — `--print-ink` on `--print-paper` is dark-on-light by default; keep any per-theme override readable ([WCAG 2.2 SC 1.4.3 Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html))
|
|
81
|
+
|
|
82
|
+
## Framework examples
|
|
83
|
+
|
|
84
|
+
The letterhead is static markup — no props, no lifecycle, no client code. Every
|
|
85
|
+
stack renders the same header; the visibility and styling live entirely in the
|
|
86
|
+
`@media print` CSS above.
|
|
87
|
+
|
|
88
|
+
### React
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
export function Letterhead() {
|
|
92
|
+
return (
|
|
93
|
+
<header className="letterhead">
|
|
94
|
+
<p className="letterhead-name">ACME CORPORATION</p>
|
|
95
|
+
<p className="letterhead-addr">1 Cliffside Drive · acme.example</p>
|
|
96
|
+
</header>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Vue
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<template>
|
|
105
|
+
<header class="letterhead">
|
|
106
|
+
<p class="letterhead-name">ACME CORPORATION</p>
|
|
107
|
+
<p class="letterhead-addr">1 Cliffside Drive · acme.example</p>
|
|
108
|
+
</header>
|
|
109
|
+
</template>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Svelte
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<header class="letterhead">
|
|
116
|
+
<p class="letterhead-name">ACME CORPORATION</p>
|
|
117
|
+
<p class="letterhead-addr">1 Cliffside Drive · acme.example</p>
|
|
118
|
+
</header>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Vanilla
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<header class="letterhead">
|
|
125
|
+
<p class="letterhead-name">ACME CORPORATION</p>
|
|
126
|
+
<p class="letterhead-addr">1 Cliffside Drive · acme.example</p>
|
|
127
|
+
</header>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Variants
|
|
131
|
+
|
|
132
|
+
- **Newsprint rule.** A theme (e.g. Press) overrides `--print-line` in its own
|
|
133
|
+
`@media print` block; the letterhead's rule turns that colour automatically —
|
|
134
|
+
no change to the letterhead itself.
|
|
135
|
+
- **Logo instead of a wordmark.** Swap `.letterhead-name` for an `<img>` (give
|
|
136
|
+
it `alt`). Prefer an inline SVG or a build-time data URI so nothing is fetched
|
|
137
|
+
at print time.
|
|
138
|
+
- **Footer letterhead.** The same pattern as a `print-only` footer — page-foot
|
|
139
|
+
address and a rule — using `margin-block-start: auto` inside a full-page flex
|
|
140
|
+
column to pin it to the bottom (see [`print-to-pdf`](./print-to-pdf.md)).
|
|
141
|
+
|
|
142
|
+
## Pitfalls
|
|
143
|
+
|
|
144
|
+
- **`print-base` must be included once at the stylesheet root**, or the
|
|
145
|
+
`--print-*` tokens are undefined and the rule/ink fall back to initial values.
|
|
146
|
+
- **Don't duplicate identity on screen.** Because it is `print-only`, let your
|
|
147
|
+
normal on-screen header carry the brand; the letterhead is for paper alone.
|
|
148
|
+
- **A full-bleed coloured band costs toner.** Prefer ink, a rule, and restraint;
|
|
149
|
+
let `--print-paper` stay near-white unless a theme deliberately wants a tint.
|
|
150
|
+
|
|
151
|
+
## Related recipes
|
|
152
|
+
|
|
153
|
+
- [`print-to-pdf`](./print-to-pdf.md) — the page *is* the document; save it as a
|
|
154
|
+
faithful PDF. The letterhead lives on top of this.
|
|
155
|
+
- [`print-spec`](./print-spec.md) — paginate a page into a spec document with a
|
|
156
|
+
cover, index, and printed sheet numbers.
|
|
@@ -67,16 +67,16 @@ Four moves, all pure CSS: `print-base` once at the root, `print-only` on the ind
|
|
|
67
67
|
@use 'css-is-awesome/api' as cia;
|
|
68
68
|
|
|
69
69
|
// The control plane, the @page box, the animation freeze — plus the print
|
|
70
|
-
// polish flags. Each defaults OFF
|
|
71
|
-
//
|
|
72
|
-
// $legible — dark-only themes (literal light ink) print dark body text.
|
|
70
|
+
// polish flags. Each defaults OFF, so nothing changes for existing callers
|
|
71
|
+
// until you opt in:
|
|
73
72
|
// $link-urls — every link prints its destination, so paper is followable.
|
|
74
73
|
// $link-origin — prepended to internal (/…) hrefs so they print as full URLs.
|
|
75
74
|
// $page-numbers — sheet numbers in the bottom-center margin box.
|
|
76
|
-
// print-base also forces `color-scheme: light` in print for free
|
|
77
|
-
//
|
|
75
|
+
// print-base also forces `color-scheme: light` in print for free (PAIRED
|
|
76
|
+
// light-dark() themes land on their light branch), and rebinds the theme's
|
|
77
|
+
// colour tokens onto a print palette (--print-ink/-paper/-line/-muted, default
|
|
78
|
+
// ink-on-white) so every theme — dark-only included — prints legible.
|
|
78
79
|
@include cia.print-base(
|
|
79
|
-
$legible: true,
|
|
80
80
|
$link-urls: true,
|
|
81
81
|
$link-origin: 'https://example.com',
|
|
82
82
|
$page-numbers: true
|
|
@@ -90,11 +90,14 @@ Four moves, all pure CSS: `print-base` once at the root, `print-only` on the ind
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
`--ink
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
Print colour is handled by the palette, not a flag: `print-base` rebinds the
|
|
94
|
+
theme's `--ink`, `--surface-*`, `--border-*` and `--code-*` onto `--print-ink`,
|
|
95
|
+
`--print-paper`, `--print-line` and `--print-muted` (default ink-on-white), so
|
|
96
|
+
every theme — dark-only ones included — prints legible with no setting. Override
|
|
97
|
+
a `--print-*` token to restyle paper (a navy letterhead, silver rules). The old
|
|
98
|
+
`$legible` flag is a deprecated no-op. `$link-urls` prints external hrefs
|
|
99
|
+
directly and internal ones prefixed with `$link-origin`; same-page `#anchor`
|
|
100
|
+
links are skipped (the URL adds nothing).
|
|
98
101
|
|
|
99
102
|
**2. In the page's COMPONENT stylesheet** — the index show/hide and the per-section page break emit nothing until called, so they are safe in a `.module.scss`.
|
|
100
103
|
|
package/scss/themes/press.scss
CHANGED
|
@@ -249,4 +249,24 @@
|
|
|
249
249
|
--card-border: 1px solid var(--hair); /* thin rule instead */
|
|
250
250
|
--input-radius: var(--r-sm); /* 2px */
|
|
251
251
|
--modal-radius: var(--r-md); /* 3px */
|
|
252
|
+
|
|
253
|
+
/* ----------------------------------------------------------------
|
|
254
|
+
PRINT — Press's own paper identity (opt-in per-theme override).
|
|
255
|
+
Sass hoists this @media out of the theme selector, so it emits as
|
|
256
|
+
`@media print { :root, :root[data-theme="press"] { … } }` and
|
|
257
|
+
overrides print-base's default palette on paper only. Newsprint:
|
|
258
|
+
a faint warm stock, warm near-black ink, press-red masthead rules.
|
|
259
|
+
Every other theme inherits print-base's plain ink-on-white.
|
|
260
|
+
---------------------------------------------------------------- */
|
|
261
|
+
@media print {
|
|
262
|
+
/* stylelint-disable no-invalid-position-declaration -- these are custom-property
|
|
263
|
+
overrides scoped to the theme selector m.theme() supplies; the position is valid
|
|
264
|
+
once the mixin wraps them (emits `@media print { :root, :root[data-theme=press] }`),
|
|
265
|
+
but stylelint can't see the mixin's parent selector statically. */
|
|
266
|
+
--print-paper: #FBFAF6; /* faint warm newsprint stock */
|
|
267
|
+
--print-ink: #0A0A0A; /* warm near-black */
|
|
268
|
+
--print-line: #D93025; /* press red — the masthead rule */
|
|
269
|
+
--print-muted: #6B6963; /* warm editorial gray */
|
|
270
|
+
/* stylelint-enable no-invalid-position-declaration */
|
|
271
|
+
}
|
|
252
272
|
}
|