css-is-awesome 1.19.2 → 1.21.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/AGENTS.md +11 -5
- package/CHANGELOG.md +20 -0
- package/CONTRACT.md +10 -0
- package/MIGRATION.md +58 -0
- package/README.md +19 -11
- package/bin/analyze.cjs +47 -2
- package/bin/cia.cjs +10 -0
- package/bin/fix-theme.cjs +151 -0
- package/llm.txt +5 -5
- package/mcp/server.cjs +64 -3
- package/package.json +6 -3
- package/public/theme.css +138 -0
- package/public/themes/boilerplate/theme.css +6 -0
- package/public/themes/boilerplate-dark/theme.css +6 -0
- package/public/themes/boilerplate-light/theme.css +6 -0
- package/public/themes/cupertino/theme.css +6 -0
- package/public/themes/cupertino-dark/theme.css +6 -0
- package/public/themes/cupertino-light/theme.css +6 -0
- package/public/themes/glass/theme.css +6 -0
- package/public/themes/glass-dark/theme.css +6 -0
- package/public/themes/glass-light/theme.css +6 -0
- package/public/themes/graphite/theme.css +6 -0
- package/public/themes/graphite-dark/theme.css +6 -0
- package/public/themes/graphite-light/theme.css +6 -0
- package/public/themes/press/theme.css +6 -0
- package/public/themes/press-dark/theme.css +6 -0
- package/public/themes/press-light/theme.css +6 -0
- package/public/themes/prism/theme.css +6 -0
- package/public/themes/prism-dark/theme.css +6 -0
- package/public/themes/prism-light/theme.css +6 -0
- package/public/themes/sketchbook/theme.css +6 -0
- package/public/themes/sketchbook-dark/theme.css +6 -0
- package/public/themes/sketchbook-light/theme.css +6 -0
- package/public/themes/terminal/theme.css +6 -0
- package/public/themes/terminal-dark/theme.css +6 -0
- package/public/themes/terminal-light/theme.css +6 -0
- package/scripts/audit-pairs.json +171 -23
- package/scripts/fix-theme.cjs +184 -0
- package/scripts/theme-contract.json +33 -4
- package/scripts/theme-validator.js +40 -1
- package/scss/_layout.scss +129 -0
|
@@ -202,4 +202,10 @@
|
|
|
202
202
|
--z-modal: 1050;
|
|
203
203
|
--z-popover: 1060;
|
|
204
204
|
--z-tooltip: 1070;
|
|
205
|
+
|
|
206
|
+
/* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
|
|
207
|
+
--page-hero-bg: #1f1f1f;
|
|
208
|
+
--page-hero-ink: #f4efe2;
|
|
209
|
+
--page-band-bg: #1d1c1b;
|
|
210
|
+
--page-band-ink: #f4efe2;
|
|
205
211
|
}
|
|
@@ -189,4 +189,10 @@
|
|
|
189
189
|
--space-7: calc(var(--space-unit) * 24);
|
|
190
190
|
--space-8: calc(var(--space-unit) * 32);
|
|
191
191
|
--space-9: calc(var(--space-unit) * 48);
|
|
192
|
+
|
|
193
|
+
/* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
|
|
194
|
+
--page-hero-bg: #eae8e2;
|
|
195
|
+
--page-hero-ink: #2a241e;
|
|
196
|
+
--page-band-bg: #efede5;
|
|
197
|
+
--page-band-ink: #2a241e;
|
|
192
198
|
}
|
|
@@ -167,4 +167,10 @@
|
|
|
167
167
|
--input-radius: 0;
|
|
168
168
|
--card-shadow: var(--glow-sm);
|
|
169
169
|
--card-border: 1px solid var(--ink-faint);
|
|
170
|
+
|
|
171
|
+
/* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
|
|
172
|
+
--page-hero-bg: #111a19;
|
|
173
|
+
--page-hero-ink: #4fe078;
|
|
174
|
+
--page-band-bg: #0e1513;
|
|
175
|
+
--page-band-ink: #4fe078;
|
|
170
176
|
}
|
|
@@ -237,4 +237,10 @@
|
|
|
237
237
|
--input-radius: 0;
|
|
238
238
|
--card-shadow: var(--glow-sm);
|
|
239
239
|
--card-border: 1px solid var(--ink-faint);
|
|
240
|
+
|
|
241
|
+
/* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
|
|
242
|
+
--page-hero-bg: #111a19;
|
|
243
|
+
--page-hero-ink: #4fe078;
|
|
244
|
+
--page-band-bg: #0e1513;
|
|
245
|
+
--page-band-ink: #4fe078;
|
|
240
246
|
}
|
|
@@ -160,4 +160,10 @@
|
|
|
160
160
|
--z-modal: 1050;
|
|
161
161
|
--z-popover: 1060;
|
|
162
162
|
--z-tooltip: 1070;
|
|
163
|
+
|
|
164
|
+
/* page surfaces — derived, see scripts/derive-page-surfaces.mjs */
|
|
165
|
+
--page-hero-bg: #dadfe4;
|
|
166
|
+
--page-hero-ink: #1a1a1a;
|
|
167
|
+
--page-band-bg: #e0e3e6;
|
|
168
|
+
--page-band-ink: #1a1a1a;
|
|
163
169
|
}
|
package/scripts/audit-pairs.json
CHANGED
|
@@ -1,25 +1,173 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
2
|
+
{
|
|
3
|
+
"fg": "--text-primary",
|
|
4
|
+
"bg": "--paper",
|
|
5
|
+
"required": 4.5,
|
|
6
|
+
"kind": "text",
|
|
7
|
+
"note": "primary body text on the page background"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"fg": "--text-secondary",
|
|
11
|
+
"bg": "--paper",
|
|
12
|
+
"required": 4.5,
|
|
13
|
+
"kind": "text",
|
|
14
|
+
"note": "secondary body text on the page background"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"fg": "--text-tertiary",
|
|
18
|
+
"bg": "--paper",
|
|
19
|
+
"required": 3,
|
|
20
|
+
"kind": "large-text",
|
|
21
|
+
"warnIfBelow": 4.5,
|
|
22
|
+
"note": "tertiary text - legal at large sizes; warn if used as body"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"fg": "--text-muted",
|
|
26
|
+
"bg": "--paper",
|
|
27
|
+
"required": 4.5,
|
|
28
|
+
"kind": "text",
|
|
29
|
+
"note": "muted UI text on the page background"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"fg": "--ink",
|
|
33
|
+
"bg": "--paper",
|
|
34
|
+
"required": 4.5,
|
|
35
|
+
"kind": "text",
|
|
36
|
+
"note": "native ink on native paper"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"fg": "--ink-soft",
|
|
40
|
+
"bg": "--paper",
|
|
41
|
+
"required": 4.5,
|
|
42
|
+
"kind": "text",
|
|
43
|
+
"note": "soft ink on native paper"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"fg": "--ink-faint",
|
|
47
|
+
"bg": "--paper",
|
|
48
|
+
"required": 3,
|
|
49
|
+
"kind": "large-text",
|
|
50
|
+
"warnIfBelow": 4.5,
|
|
51
|
+
"note": "faint ink - large/UI only; warn if used as body"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"fg": "--text-link",
|
|
55
|
+
"bg": "--paper",
|
|
56
|
+
"required": 4.5,
|
|
57
|
+
"kind": "text",
|
|
58
|
+
"note": "inline link color on the page background"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"fg": "--text-inverse",
|
|
62
|
+
"bg": "--action-primary-default",
|
|
63
|
+
"required": 4.5,
|
|
64
|
+
"kind": "text",
|
|
65
|
+
"note": "button label on the primary action surface"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"fg": "--text-inverse",
|
|
69
|
+
"bg": "--ai",
|
|
70
|
+
"required": 4.5,
|
|
71
|
+
"kind": "text",
|
|
72
|
+
"note": "inverse text on the AI/brand accent surface"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"fg": "--success-text",
|
|
76
|
+
"bg": "--success-subtle",
|
|
77
|
+
"required": 4.5,
|
|
78
|
+
"kind": "text",
|
|
79
|
+
"note": "success message text on its subtle background"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"fg": "--warning-text",
|
|
83
|
+
"bg": "--warning-subtle",
|
|
84
|
+
"required": 4.5,
|
|
85
|
+
"kind": "text",
|
|
86
|
+
"note": "warning message text on its subtle background"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"fg": "--error-text",
|
|
90
|
+
"bg": "--error-subtle",
|
|
91
|
+
"required": 4.5,
|
|
92
|
+
"kind": "text",
|
|
93
|
+
"note": "error message text on its subtle background"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"fg": "--info-text",
|
|
97
|
+
"bg": "--info-subtle",
|
|
98
|
+
"required": 4.5,
|
|
99
|
+
"kind": "text",
|
|
100
|
+
"note": "info message text on its subtle background"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"fg": "--border-default",
|
|
104
|
+
"bg": "--paper",
|
|
105
|
+
"required": 3,
|
|
106
|
+
"kind": "non-text",
|
|
107
|
+
"decorative": true,
|
|
108
|
+
"note": "default border against the page background - decorative; WCAG 2.2 SC 1.4.11 applies only to graphical objects essential for understanding content"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"fg": "--border-focus",
|
|
112
|
+
"bg": "--paper",
|
|
113
|
+
"required": 3,
|
|
114
|
+
"kind": "non-text",
|
|
115
|
+
"note": "focus ring against the page background"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"fg": "--shu",
|
|
119
|
+
"bg": "--paper",
|
|
120
|
+
"required": 3,
|
|
121
|
+
"kind": "non-text",
|
|
122
|
+
"note": "seal accent - must read as a UI mark on paper"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"fg": "--code-ink",
|
|
126
|
+
"bg": "--code-bg",
|
|
127
|
+
"required": 4.5,
|
|
128
|
+
"kind": "text",
|
|
129
|
+
"note": "default code text on the code surface"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"fg": "--code-muted",
|
|
133
|
+
"bg": "--code-bg",
|
|
134
|
+
"required": 4.5,
|
|
135
|
+
"kind": "text",
|
|
136
|
+
"note": "code comments on the code surface - read as prose, so full text contrast"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"fg": "--code-accent",
|
|
140
|
+
"bg": "--code-bg",
|
|
141
|
+
"required": 4.5,
|
|
142
|
+
"kind": "text",
|
|
143
|
+
"note": "keyword / accent token on the code surface"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"fg": "--code-blue",
|
|
147
|
+
"bg": "--code-bg",
|
|
148
|
+
"required": 4.5,
|
|
149
|
+
"kind": "text",
|
|
150
|
+
"note": "blue syntax token on the code surface"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"fg": "--code-green",
|
|
154
|
+
"bg": "--code-bg",
|
|
155
|
+
"required": 4.5,
|
|
156
|
+
"kind": "text",
|
|
157
|
+
"note": "green syntax token (strings) on the code surface"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"fg": "--page-hero-ink",
|
|
161
|
+
"bg": "--page-hero-bg",
|
|
162
|
+
"required": 4.5,
|
|
163
|
+
"kind": "text",
|
|
164
|
+
"note": "text on the hero page surface - graded only when the theme declares the slot"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"fg": "--page-band-ink",
|
|
168
|
+
"bg": "--page-band-bg",
|
|
169
|
+
"required": 4.5,
|
|
170
|
+
"kind": "text",
|
|
171
|
+
"note": "text on the band page surface - graded only when the theme declares the slot"
|
|
172
|
+
}
|
|
25
173
|
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// fix-theme.cjs
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Rewrite a theme's DEPRECATED token declarations to their replacements.
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS EXISTS
|
|
8
|
+
// Until now the tooling could only *say* a token was deprecated. A consumer
|
|
9
|
+
// then had to find it, look up what replaced it, and edit by hand — for a
|
|
10
|
+
// rename that a machine can do exactly. `scripts/theme-contract.json` already
|
|
11
|
+
// carries the `deprecated` map (old token → replacedBy / since / removeIn /
|
|
12
|
+
// note); this turns that map into an offer: here is what changed, here is the
|
|
13
|
+
// corrected file, say the word and it is yours.
|
|
14
|
+
//
|
|
15
|
+
// WHAT IT WILL NOT DO
|
|
16
|
+
// * It never writes to disk. It returns text. The caller decides — same
|
|
17
|
+
// contract as themeFromTokens(), for the same reason: a tool that edits
|
|
18
|
+
// a consumer's file as a side effect of being asked a question is a tool
|
|
19
|
+
// nobody can trust in a pipeline.
|
|
20
|
+
// * It never changes a VALUE. Only the property name on the left of the
|
|
21
|
+
// colon moves. A rename cannot alter how the theme looks.
|
|
22
|
+
// * It never duplicates. If a block already declares the replacement, the
|
|
23
|
+
// old line is left exactly where it is and the collision is reported,
|
|
24
|
+
// because merging two values is a judgement call, not a rewrite.
|
|
25
|
+
//
|
|
26
|
+
// FORMATTING
|
|
27
|
+
// Line-based on purpose. A CSS parser would round-trip the file through an
|
|
28
|
+
// AST and quietly restyle whitespace, comment placement and declaration
|
|
29
|
+
// order — a diff full of noise around the one line that mattered. Here the
|
|
30
|
+
// bytes either side of the property name survive untouched.
|
|
31
|
+
//
|
|
32
|
+
// Usage (library):
|
|
33
|
+
// const { fixTheme } = require('css-is-awesome/scripts/fix-theme.cjs');
|
|
34
|
+
// const { css, changes, unchanged } = fixTheme({ css: themeText });
|
|
35
|
+
// ============================================================================
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
const CONTRACT_PATH = path.join(__dirname, 'theme-contract.json');
|
|
42
|
+
|
|
43
|
+
/** Load the token contract, or throw something a human can act on. */
|
|
44
|
+
function loadContract(ciaRoot) {
|
|
45
|
+
const p = ciaRoot ? path.join(ciaRoot, 'scripts', 'theme-contract.json') : CONTRACT_PATH;
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`fix-theme: could not read the token contract at ${p} — ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`fix-theme: the token contract at ${p} is not valid JSON — ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The deprecations as a flat, serialisable list — the same data `get_token`
|
|
61
|
+
* and the validator report, so all three can never disagree.
|
|
62
|
+
*/
|
|
63
|
+
function deprecations(opts) {
|
|
64
|
+
const contract = (opts && opts.contract) || loadContract(opts && opts.ciaRoot);
|
|
65
|
+
const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
66
|
+
return Object.entries(map).map(([token, d]) => ({
|
|
67
|
+
token,
|
|
68
|
+
replacedBy: (d && d.replacedBy) || null,
|
|
69
|
+
since: (d && d.since) || null,
|
|
70
|
+
removeIn: (d && d.removeIn) || null,
|
|
71
|
+
note: (d && d.note) || null,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A custom-property declaration: leading whitespace, the name, optional
|
|
76
|
+
// whitespace, the colon. Everything from the colon rightwards is untouched.
|
|
77
|
+
const DECL = /^(\s*)(--[A-Za-z0-9_-]+)(\s*):/;
|
|
78
|
+
|
|
79
|
+
// Strip /* … */ so a commented-out declaration is never counted as declared
|
|
80
|
+
// nor rewritten. Length is preserved so line numbers stay honest.
|
|
81
|
+
function blankComments(text) {
|
|
82
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Rewrite deprecated declarations.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} o
|
|
89
|
+
* @param {string} o.css theme CSS (compiled, not .scss source)
|
|
90
|
+
* @param {object} [o.contract] pre-loaded contract (tests / callers with one)
|
|
91
|
+
* @param {string} [o.ciaRoot] resolve the contract from another install
|
|
92
|
+
* @returns {{css:string, changes:Array, unchanged:boolean, deprecatedFound:string[]}}
|
|
93
|
+
* changes[] entries are `{ line, from, to, reason, kind }` where kind is
|
|
94
|
+
* 'rewrite' (applied) or 'conflict' (left alone, and why).
|
|
95
|
+
*/
|
|
96
|
+
function fixTheme(o) {
|
|
97
|
+
const opts = o || {};
|
|
98
|
+
if (typeof opts.css !== 'string') {
|
|
99
|
+
throw new Error('fix-theme: css is required and must be a string (compiled CSS, not .scss source)');
|
|
100
|
+
}
|
|
101
|
+
const contract = opts.contract || loadContract(opts.ciaRoot);
|
|
102
|
+
const map = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
103
|
+
|
|
104
|
+
const lines = opts.css.split('\n');
|
|
105
|
+
const scan = blankComments(opts.css).split('\n');
|
|
106
|
+
|
|
107
|
+
// ── Pass 1: which tokens does each brace-scope already declare? ──────────
|
|
108
|
+
// Scope id changes on every `{`, so two theme blocks in one bundled file
|
|
109
|
+
// are judged independently — a collision in `[data-theme="a"]` says nothing
|
|
110
|
+
// about `[data-theme="b"]`.
|
|
111
|
+
const scopeOf = new Array(lines.length).fill(0);
|
|
112
|
+
const declaredIn = new Map(); // scopeId -> Set<token>
|
|
113
|
+
{
|
|
114
|
+
let depth = 0;
|
|
115
|
+
const stack = [0];
|
|
116
|
+
let next = 1;
|
|
117
|
+
for (let i = 0; i < scan.length; i++) {
|
|
118
|
+
const line = scan[i];
|
|
119
|
+
scopeOf[i] = stack[stack.length - 1];
|
|
120
|
+
const m = DECL.exec(line);
|
|
121
|
+
if (m) {
|
|
122
|
+
const id = stack[stack.length - 1];
|
|
123
|
+
if (!declaredIn.has(id)) declaredIn.set(id, new Set());
|
|
124
|
+
declaredIn.get(id).add(m[2]);
|
|
125
|
+
}
|
|
126
|
+
for (const ch of line) {
|
|
127
|
+
if (ch === '{') { stack.push(next++); depth++; }
|
|
128
|
+
else if (ch === '}') { if (stack.length > 1) stack.pop(); depth--; }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Pass 2: rewrite, or explain why not ─────────────────────────────────
|
|
134
|
+
const changes = [];
|
|
135
|
+
const found = new Set();
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const m = DECL.exec(scan[i]);
|
|
138
|
+
if (!m) continue;
|
|
139
|
+
const token = m[2];
|
|
140
|
+
const dep = map[token];
|
|
141
|
+
if (!dep || !dep.replacedBy) continue;
|
|
142
|
+
|
|
143
|
+
found.add(token);
|
|
144
|
+
const replacement = dep.replacedBy;
|
|
145
|
+
const already = declaredIn.get(scopeOf[i]);
|
|
146
|
+
|
|
147
|
+
if (already && already.has(replacement)) {
|
|
148
|
+
changes.push({
|
|
149
|
+
line: i + 1,
|
|
150
|
+
from: token,
|
|
151
|
+
to: replacement,
|
|
152
|
+
kind: 'conflict',
|
|
153
|
+
reason:
|
|
154
|
+
`${replacement} is already declared in this block, so renaming ${token} would ` +
|
|
155
|
+
`produce two declarations of the same property and silently pick the last one. ` +
|
|
156
|
+
`Left untouched — decide which value you want and delete the other.`,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rename only the property. Indentation, spacing before the colon, the
|
|
162
|
+
// value, the semicolon and any trailing comment all survive byte-exact.
|
|
163
|
+
lines[i] = lines[i].replace(DECL, (_full, indent, _name, gap) => `${indent}${replacement}${gap}:`);
|
|
164
|
+
changes.push({
|
|
165
|
+
line: i + 1,
|
|
166
|
+
from: token,
|
|
167
|
+
to: replacement,
|
|
168
|
+
kind: 'rewrite',
|
|
169
|
+
reason:
|
|
170
|
+
`${token} is deprecated since contract ${dep.since || '?'} and is removed in ` +
|
|
171
|
+
`${dep.removeIn || 'a future major'}. ${dep.note || ''}`.trim(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rewrites = changes.filter((c) => c.kind === 'rewrite');
|
|
176
|
+
return {
|
|
177
|
+
css: rewrites.length ? lines.join('\n') : opts.css,
|
|
178
|
+
changes,
|
|
179
|
+
unchanged: rewrites.length === 0,
|
|
180
|
+
deprecatedFound: [...found].sort(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { fixTheme, deprecations, loadContract };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures
|
|
2
|
+
"version": "1.3",
|
|
3
|
+
"description": "Authoritative token contract for css-is-awesome themes. Every theme.css MUST declare every token in `required`. Tokens in `optional` are reported by the validator as info, never as failures — the library or the theme generator supplies a default. Source of truth is CONTRACT.md (human-readable) and public/theme.css (Sketchbook reference). Contract 1.3 (2026-09-24): the page-surfaces feature adds eight optional tokens for the hero and band surfaces, and --background-hero is deprecated in favour of --page-hero-bg (kept valid, removed no earlier than contract 2).",
|
|
4
4
|
"required": [
|
|
5
5
|
"--action-primary-active",
|
|
6
6
|
"--action-primary-default",
|
|
@@ -151,6 +151,14 @@
|
|
|
151
151
|
"--logo-monochrome",
|
|
152
152
|
"--logo-wordmark",
|
|
153
153
|
"--modal-radius",
|
|
154
|
+
"--page-band-bg",
|
|
155
|
+
"--page-band-image",
|
|
156
|
+
"--page-band-ink",
|
|
157
|
+
"--page-band-scrim",
|
|
158
|
+
"--page-hero-bg",
|
|
159
|
+
"--page-hero-image",
|
|
160
|
+
"--page-hero-ink",
|
|
161
|
+
"--page-hero-scrim",
|
|
154
162
|
"--print-ink",
|
|
155
163
|
"--print-line",
|
|
156
164
|
"--print-muted",
|
|
@@ -181,7 +189,7 @@
|
|
|
181
189
|
]
|
|
182
190
|
},
|
|
183
191
|
"spacing-aliases": {
|
|
184
|
-
"enables": "T-shirt spacing names (2xs
|
|
192
|
+
"enables": "T-shirt spacing names (2xs–xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases.",
|
|
185
193
|
"tokens": [
|
|
186
194
|
"--space-2xs",
|
|
187
195
|
"--space-xs",
|
|
@@ -236,7 +244,6 @@
|
|
|
236
244
|
"enables": "Extra background layers (elevated, hero, overlay, scrim) for themes that want more than the required surface set; each falls back to a required surface.",
|
|
237
245
|
"tokens": [
|
|
238
246
|
"--background-elevated",
|
|
239
|
-
"--background-hero",
|
|
240
247
|
"--background-overlay",
|
|
241
248
|
"--background-scrim"
|
|
242
249
|
]
|
|
@@ -264,6 +271,28 @@
|
|
|
264
271
|
"tokens": [
|
|
265
272
|
"--touch-target-min"
|
|
266
273
|
]
|
|
274
|
+
},
|
|
275
|
+
"page-surfaces": {
|
|
276
|
+
"enables": "Two page-level surfaces a theme owns outright: a hero for the front page and a band for section stripes and footers. Each carries a background colour, an optional gradient or url() image, an ink colour for text on it, and a scrim that keeps that text legible over an image. Applied only via cia.surface() or [data-surface] - declaring them changes nothing on its own.",
|
|
277
|
+
"tokens": [
|
|
278
|
+
"--page-hero-bg",
|
|
279
|
+
"--page-hero-image",
|
|
280
|
+
"--page-hero-ink",
|
|
281
|
+
"--page-hero-scrim",
|
|
282
|
+
"--page-band-bg",
|
|
283
|
+
"--page-band-image",
|
|
284
|
+
"--page-band-ink",
|
|
285
|
+
"--page-band-scrim",
|
|
286
|
+
"--background-hero"
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"deprecated": {
|
|
291
|
+
"--background-hero": {
|
|
292
|
+
"replacedBy": "--page-hero-bg",
|
|
293
|
+
"since": "1.3",
|
|
294
|
+
"removeIn": "2",
|
|
295
|
+
"note": "Was never read by the library. --page-hero-bg falls back through it, so an existing declaration keeps working untouched."
|
|
267
296
|
}
|
|
268
297
|
}
|
|
269
298
|
}
|
|
@@ -354,7 +354,24 @@ function validateTokenSet(declared, contract) {
|
|
|
354
354
|
const f = featureOf[t] || 'other';
|
|
355
355
|
(optionalMissingByFeature[f] = optionalMissingByFeature[f] || []).push(t);
|
|
356
356
|
}
|
|
357
|
-
|
|
357
|
+
// Contract 1.3+: a token this theme declares that has since been superseded.
|
|
358
|
+
// Reported separately from the optional-token info line — "you declared
|
|
359
|
+
// something that still works but has a better name now" is a different
|
|
360
|
+
// message from "you left a default to the library", and it comes with an
|
|
361
|
+
// action: `cia fix-theme`.
|
|
362
|
+
const deprecatedMap = contract.deprecated && typeof contract.deprecated === 'object' ? contract.deprecated : {};
|
|
363
|
+
const deprecatedUsed = [];
|
|
364
|
+
for (const [token, def] of Object.entries(deprecatedMap)) {
|
|
365
|
+
if (declared.has(token)) {
|
|
366
|
+
deprecatedUsed.push({
|
|
367
|
+
token,
|
|
368
|
+
replacedBy: (def && def.replacedBy) || null,
|
|
369
|
+
since: (def && def.since) || null,
|
|
370
|
+
removeIn: (def && def.removeIn) || null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, deprecatedUsed, declaredCount: declared.size };
|
|
358
375
|
}
|
|
359
376
|
|
|
360
377
|
// -----------------------------------------------------------
|
|
@@ -380,6 +397,7 @@ function validateText(text, contract, options) {
|
|
|
380
397
|
missing: [],
|
|
381
398
|
optionalMissing: [],
|
|
382
399
|
optionalMissingByFeature: {},
|
|
400
|
+
deprecatedUsed: [],
|
|
383
401
|
themes: null,
|
|
384
402
|
a11y: null,
|
|
385
403
|
error: null,
|
|
@@ -403,6 +421,7 @@ function validateText(text, contract, options) {
|
|
|
403
421
|
missing: v.missing,
|
|
404
422
|
optionalMissing: v.optionalMissing,
|
|
405
423
|
optionalMissingByFeature: v.optionalMissingByFeature,
|
|
424
|
+
deprecatedUsed: v.deprecatedUsed,
|
|
406
425
|
a11y: null,
|
|
407
426
|
};
|
|
408
427
|
if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
|
|
@@ -432,6 +451,7 @@ function validateText(text, contract, options) {
|
|
|
432
451
|
result.missing = v.missing;
|
|
433
452
|
result.optionalMissing = v.optionalMissing;
|
|
434
453
|
result.optionalMissingByFeature = v.optionalMissingByFeature;
|
|
454
|
+
result.deprecatedUsed = v.deprecatedUsed;
|
|
435
455
|
result.ok = v.ok;
|
|
436
456
|
if (wantA11y) {
|
|
437
457
|
const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
|
|
@@ -471,6 +491,21 @@ function relForDisplay(p) {
|
|
|
471
491
|
|
|
472
492
|
// Optional tokens a theme leaves to the library default. Info only — shown as
|
|
473
493
|
// a count, or listed with --show-optional. Never affects the exit code.
|
|
494
|
+
// A token the theme declares that has been superseded. Never a failure — the
|
|
495
|
+
// old value still works — but it names the replacement and the way to apply it.
|
|
496
|
+
function deprecationInfo(deprecatedUsed, indent) {
|
|
497
|
+
const list = Array.isArray(deprecatedUsed) ? deprecatedUsed : [];
|
|
498
|
+
if (!list.length) return;
|
|
499
|
+
for (const d of list) {
|
|
500
|
+
console.log(
|
|
501
|
+
`${indent}${yellow('~')} ${d.token} is deprecated` +
|
|
502
|
+
(d.since ? ` (since contract ${d.since}` + (d.removeIn ? `, removed in ${d.removeIn}` : '') + ')' : '') +
|
|
503
|
+
(d.replacedBy ? ` — use ${d.replacedBy}` : '')
|
|
504
|
+
);
|
|
505
|
+
console.log(`${indent} ${dim('your value still works; `npx cia fix-theme <file>` renames it for you')}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
474
509
|
function optionalInfo(optionalMissing, byFeature, indent) {
|
|
475
510
|
const list = Array.isArray(optionalMissing) ? optionalMissing : [];
|
|
476
511
|
if (!list.length) return;
|
|
@@ -506,6 +541,7 @@ function reportResult(result) {
|
|
|
506
541
|
` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
|
|
507
542
|
);
|
|
508
543
|
optionalInfo(t.optionalMissing, t.optionalMissingByFeature, ' ');
|
|
544
|
+
deprecationInfo(t.deprecatedUsed, ' ');
|
|
509
545
|
} else {
|
|
510
546
|
const n = t.missing.length;
|
|
511
547
|
console.log(
|
|
@@ -514,6 +550,7 @@ function reportResult(result) {
|
|
|
514
550
|
for (const token of t.missing) {
|
|
515
551
|
console.log(` ${red(token)}`);
|
|
516
552
|
}
|
|
553
|
+
deprecationInfo(t.deprecatedUsed, ' ');
|
|
517
554
|
}
|
|
518
555
|
}
|
|
519
556
|
return;
|
|
@@ -525,6 +562,7 @@ function reportResult(result) {
|
|
|
525
562
|
`${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
|
|
526
563
|
);
|
|
527
564
|
optionalInfo(result.optionalMissing, result.optionalMissingByFeature, ' ');
|
|
565
|
+
deprecationInfo(result.deprecatedUsed, ' ');
|
|
528
566
|
return;
|
|
529
567
|
}
|
|
530
568
|
|
|
@@ -535,6 +573,7 @@ function reportResult(result) {
|
|
|
535
573
|
for (const token of result.missing) {
|
|
536
574
|
console.log(` ${red(token)}`);
|
|
537
575
|
}
|
|
576
|
+
deprecationInfo(result.deprecatedUsed, ' ');
|
|
538
577
|
}
|
|
539
578
|
|
|
540
579
|
// -----------------------------------------------------------
|