css-is-awesome 1.17.0 → 1.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ # [1.18.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.17.0...v1.18.0) (2026-09-19)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **site:** playground editor no longer boots from stale text when a share link decodes early ([3f0748f](https://github.com/Jerry2d3d/css-is-awesome/commit/3f0748f28b8a70471f1f61485733872c6ccdd6a9))
9
+
10
+
11
+ ### Features
12
+
13
+ * **contract:** feature groups for optional tokens — contract 1.2 ([cc98e1f](https://github.com/Jerry2d3d/css-is-awesome/commit/cc98e1f9397ae89ef65b70557a942b5b561a0f85))
14
+
3
15
  # [1.17.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.16.1...v1.17.0) (2026-09-19)
4
16
 
5
17
 
package/CONTRACT.md CHANGED
@@ -306,6 +306,25 @@ Paper themes declare these as `none` / `transparent` so a swap to a glass or pho
306
306
 
307
307
  ---
308
308
 
309
+ ## Optional tokens by feature (contract 1.2)
310
+
311
+ Every optional token belongs to exactly one **feature** in `scripts/theme-contract.json` (`features`), so a validator, installer or agent can say *"this theme is missing the tokens for print"* instead of listing all 41 optional names. The validator's info line reports counts per feature; `--show-optional` lists them grouped. `npm run check:contract` fails the build if an optional token is in no feature, in two, or if a feature names a required token.
312
+
313
+ | Feature | Tokens | Enables |
314
+ | --- | --- | --- |
315
+ | `density` | `--space-unit` | The density knob: shipped themes derive every --space-N from this one unit via calc(); set it to tighten or open up the whole UI (theme editor slider). |
316
+ | `spacing-aliases` | `--space-2xs`, `--space-xs`, `--space-sm`, `--space-md`, `--space-lg`, `--space-xl` | T-shirt spacing names (2xs–xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases. |
317
+ | `print` | `--print-ink`, `--print-paper`, `--print-line`, `--print-muted` | A themed printed page: cia.print-base rebinds ink/surface/border/code tokens onto this palette inside @media print. Without them the ink-on-white default applies. |
318
+ | `component-radius` | `--btn-radius`, `--card-radius`, `--input-radius`, `--modal-radius`, `--badge-radius`, `--tag-radius` | Per-component corner radius overrides (buttons, cards, inputs, modals, badges, tags) without rebuilding SCSS; each falls back to the generic --radius-* scale. |
319
+ | `component-shadows` | `--shadow-button`, `--shadow-card`, `--shadow-dropdown`, `--shadow-input-focus`, `--shadow-modal`, `--shadow-popover`, `--shadow-tooltip`, `--shadow-text` | Per-component elevation overrides; each falls back to the generic --shadow-* scale. |
320
+ | `component-motion` | `--duration-button-hover`, `--duration-modal-open`, `--duration-toast-slide` | Per-component durations for button hover, modal open and toast slide; each falls back to the generic --duration-* scale. |
321
+ | `surfaces-extended` | `--background-elevated`, `--background-hero`, `--background-overlay`, `--background-scrim` | Extra background layers (elevated, hero, overlay, scrim) for themes that want more than the required surface set; each falls back to a required surface. |
322
+ | `borders-extended` | `--border-card`, `--border-divider`, `--border-focus-ring`, `--border-input` | Per-context border colours (card, divider, focus ring, input); each falls back to --border-default / --border-focus. |
323
+ | `logo` | `--logo-default`, `--logo-mark`, `--logo-monochrome`, `--logo-wordmark` | Theme-aware logo assets (default, mark, monochrome, wordmark) as url() or SVG data values for the icon/brand mixins. |
324
+ | `touch-target` | `--touch-target-min` | Minimum interactive target size read by form and button mixins (WCAG 2.2 SC 2.5.8); the library default is 24px. |
325
+
326
+ ---
327
+
309
328
  ## Print (optional)
310
329
 
311
330
  Four tokens describing the printed page. They're **optional** — `cia.print-base` (included once, at the stylesheet root) emits every one of them on `:root` inside `@media print` with a clean ink-on-white default, and every print rule reads them via `var(--print-*)`. A theme MAY override them in its own `@media print` block for a paper identity (Press does, for a newsprint look); a theme that sets none of them prints the plain default.
package/VERSIONING.md CHANGED
@@ -43,6 +43,7 @@ Additive, non-breaking changes.
43
43
  | New public CSS class | `.cia-grid-auto-fit` added |
44
44
  | New public SCSS mixin | `m.cluster($gap)` added |
45
45
  | New optional token added to contract (`"1"` → `"1.1"`) | `--dropdown-offset-y` added to component section |
46
+ | Contract metadata added (e.g. the `features` map, `"1.1"` → `"1.2"`, 2026-09-18) | Additive keys — old validators ignore them |
46
47
  | Required token relaxed to optional (contract minor bump) | `--space-unit` required → optional, contract `"1"` → `"1.1"` (2026-09-18) |
47
48
  | New theme or recipe shipped | `prism` family added; `mobile-nav` recipe added |
48
49
  | New utility class (`.cia-*`) | `.cia-text-balance` added |
package/llm.txt CHANGED
@@ -104,7 +104,7 @@ Newspaper by day, hacker terminal by night. Most design systems give you dark mo
104
104
  | Mobile playbook (layouts, dropdown doctrine, lessons) | `src/app/docs/mobile/page.tsx` |
105
105
  | Browser support matrix (Baseline floor + progressive tiers) | `src/app/docs/browser-support/page.tsx` |
106
106
  | Theme authoring (full walkthrough) | `src/app/docs/authoring/themes/page.tsx` |
107
- | Theme contract (127 required + 41 optional, machine-readable) | `scripts/theme-contract.json` |
107
+ | Theme contract (127 required + 41 optional grouped into 10 features, machine-readable) | `scripts/theme-contract.json` |
108
108
  | Theme pairing (`<link media>` recipe) | `src/app/docs/themes/pairing/page.tsx` |
109
109
  | CopyButton JS recipe | `src/app/docs/recipes/copy-button/page.tsx` |
110
110
  | Anchor positioning recipe | `src/app/docs/recipes/anchor-positioning/page.tsx` |
package/mcp/server.cjs CHANGED
@@ -426,6 +426,12 @@ function loadTokenContract() {
426
426
  return { required: [], optional: [], byName: {}, byCategory: {} };
427
427
  }
428
428
  const optional = Array.isArray(contract.optional) ? contract.optional : [];
429
+ // Contract 1.2: optional tokens are grouped by the feature they enable.
430
+ const features = contract.features && typeof contract.features === 'object' ? contract.features : {};
431
+ const featureOf = {};
432
+ for (const [feature, def] of Object.entries(features)) {
433
+ for (const t of (def && Array.isArray(def.tokens)) ? def.tokens : []) featureOf[t] = feature;
434
+ }
429
435
 
430
436
  const byName = {};
431
437
  const byCategory = {};
@@ -460,11 +466,11 @@ function loadTokenContract() {
460
466
  }
461
467
  for (const t of optional) {
462
468
  const category = categorize(t);
463
- byName[t] = { name: t, category, required: false };
469
+ byName[t] = { name: t, category, required: false, feature: featureOf[t] || null };
464
470
  (byCategory[category] = byCategory[category] || []).push(t);
465
471
  }
466
472
 
467
- return { required: contract.required, optional, byName, byCategory };
473
+ return { required: contract.required, optional, features, byName, byCategory };
468
474
  }
469
475
 
470
476
  /**
@@ -854,6 +860,8 @@ const handlers = {
854
860
  name: entry.name,
855
861
  category: entry.category,
856
862
  required: entry.required,
863
+ // Contract 1.2: the feature an OPTIONAL token enables (null for required).
864
+ feature: entry.required ? null : (entry.feature || null),
857
865
  themeValues,
858
866
  referencedBy: referencedBy.slice(0, 20),
859
867
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-is-awesome",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.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 \u2014 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.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR.",
2
+ "version": "1.2",
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 \u2014 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.1 (2026-09-18): --space-unit relaxed from required to optional; it had been added as required in library 1.12.0, which VERSIONING.md forbids in a MINOR. Contract 1.2 (2026-09-18): `features` groups every optional token by the capability it enables, so a validator or installer can say \"missing the tokens for print\" instead of listing all optional tokens.",
4
4
  "required": [
5
5
  "--action-primary-active",
6
6
  "--action-primary-default",
@@ -172,5 +172,98 @@
172
172
  "--space-xs",
173
173
  "--tag-radius",
174
174
  "--touch-target-min"
175
- ]
175
+ ],
176
+ "features": {
177
+ "density": {
178
+ "enables": "The density knob: shipped themes derive every --space-N from this one unit via calc(); set it to tighten or open up the whole UI (theme editor slider).",
179
+ "tokens": [
180
+ "--space-unit"
181
+ ]
182
+ },
183
+ "spacing-aliases": {
184
+ "enables": "T-shirt spacing names (2xs\u2013xl) for consumer CSS that prefers them; the library reads the numbered scale, so these are pure aliases.",
185
+ "tokens": [
186
+ "--space-2xs",
187
+ "--space-xs",
188
+ "--space-sm",
189
+ "--space-md",
190
+ "--space-lg",
191
+ "--space-xl"
192
+ ]
193
+ },
194
+ "print": {
195
+ "enables": "A themed printed page: cia.print-base rebinds ink/surface/border/code tokens onto this palette inside @media print. Without them the ink-on-white default applies.",
196
+ "tokens": [
197
+ "--print-ink",
198
+ "--print-paper",
199
+ "--print-line",
200
+ "--print-muted"
201
+ ]
202
+ },
203
+ "component-radius": {
204
+ "enables": "Per-component corner radius overrides (buttons, cards, inputs, modals, badges, tags) without rebuilding SCSS; each falls back to the generic --radius-* scale.",
205
+ "tokens": [
206
+ "--btn-radius",
207
+ "--card-radius",
208
+ "--input-radius",
209
+ "--modal-radius",
210
+ "--badge-radius",
211
+ "--tag-radius"
212
+ ]
213
+ },
214
+ "component-shadows": {
215
+ "enables": "Per-component elevation overrides; each falls back to the generic --shadow-* scale.",
216
+ "tokens": [
217
+ "--shadow-button",
218
+ "--shadow-card",
219
+ "--shadow-dropdown",
220
+ "--shadow-input-focus",
221
+ "--shadow-modal",
222
+ "--shadow-popover",
223
+ "--shadow-tooltip",
224
+ "--shadow-text"
225
+ ]
226
+ },
227
+ "component-motion": {
228
+ "enables": "Per-component durations for button hover, modal open and toast slide; each falls back to the generic --duration-* scale.",
229
+ "tokens": [
230
+ "--duration-button-hover",
231
+ "--duration-modal-open",
232
+ "--duration-toast-slide"
233
+ ]
234
+ },
235
+ "surfaces-extended": {
236
+ "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
+ "tokens": [
238
+ "--background-elevated",
239
+ "--background-hero",
240
+ "--background-overlay",
241
+ "--background-scrim"
242
+ ]
243
+ },
244
+ "borders-extended": {
245
+ "enables": "Per-context border colours (card, divider, focus ring, input); each falls back to --border-default / --border-focus.",
246
+ "tokens": [
247
+ "--border-card",
248
+ "--border-divider",
249
+ "--border-focus-ring",
250
+ "--border-input"
251
+ ]
252
+ },
253
+ "logo": {
254
+ "enables": "Theme-aware logo assets (default, mark, monochrome, wordmark) as url() or SVG data values for the icon/brand mixins.",
255
+ "tokens": [
256
+ "--logo-default",
257
+ "--logo-mark",
258
+ "--logo-monochrome",
259
+ "--logo-wordmark"
260
+ ]
261
+ },
262
+ "touch-target": {
263
+ "enables": "Minimum interactive target size read by form and button mixins (WCAG 2.2 SC 2.5.8); the library default is 24px.",
264
+ "tokens": [
265
+ "--touch-target-min"
266
+ ]
267
+ }
268
+ }
176
269
  }
@@ -342,7 +342,19 @@ function validateTokenSet(declared, contract) {
342
342
  for (const optional of Array.isArray(contract.optional) ? contract.optional : []) {
343
343
  if (!declared.has(optional)) optionalMissing.push(optional);
344
344
  }
345
- return { ok: missing.length === 0, missing, optionalMissing, declaredCount: declared.size };
345
+ // Contract 1.2: group by the feature each optional token enables, so the
346
+ // report can say "missing the tokens for print" instead of listing 41 names.
347
+ const featureOf = {};
348
+ const features = contract.features && typeof contract.features === 'object' ? contract.features : {};
349
+ for (const [feature, def] of Object.entries(features)) {
350
+ for (const t of (def && Array.isArray(def.tokens)) ? def.tokens : []) featureOf[t] = feature;
351
+ }
352
+ const optionalMissingByFeature = {};
353
+ for (const t of optionalMissing) {
354
+ const f = featureOf[t] || 'other';
355
+ (optionalMissingByFeature[f] = optionalMissingByFeature[f] || []).push(t);
356
+ }
357
+ return { ok: missing.length === 0, missing, optionalMissing, optionalMissingByFeature, declaredCount: declared.size };
346
358
  }
347
359
 
348
360
  // -----------------------------------------------------------
@@ -367,6 +379,7 @@ function validateText(text, contract, options) {
367
379
  declaredCount: 0,
368
380
  missing: [],
369
381
  optionalMissing: [],
382
+ optionalMissingByFeature: {},
370
383
  themes: null,
371
384
  a11y: null,
372
385
  error: null,
@@ -389,6 +402,7 @@ function validateText(text, contract, options) {
389
402
  declaredCount: v.declaredCount,
390
403
  missing: v.missing,
391
404
  optionalMissing: v.optionalMissing,
405
+ optionalMissingByFeature: v.optionalMissingByFeature,
392
406
  a11y: null,
393
407
  };
394
408
  if (wantA11y) theme.a11y = a11y.auditThemeTokens({ name: b.name, values: b.values });
@@ -417,6 +431,7 @@ function validateText(text, contract, options) {
417
431
  result.declaredCount = v.declaredCount;
418
432
  result.missing = v.missing;
419
433
  result.optionalMissing = v.optionalMissing;
434
+ result.optionalMissingByFeature = v.optionalMissingByFeature;
420
435
  result.ok = v.ok;
421
436
  if (wantA11y) {
422
437
  const inferredName = label !== '(pasted CSS)' ? (path.basename(path.dirname(label)) || path.basename(label, '.css')) : 'theme';
@@ -456,11 +471,18 @@ function relForDisplay(p) {
456
471
 
457
472
  // Optional tokens a theme leaves to the library default. Info only — shown as
458
473
  // a count, or listed with --show-optional. Never affects the exit code.
459
- function optionalInfo(optionalMissing, indent) {
474
+ function optionalInfo(optionalMissing, byFeature, indent) {
460
475
  const list = Array.isArray(optionalMissing) ? optionalMissing : [];
461
476
  if (!list.length) return;
462
- console.log(`${indent}${dim(`i ${list.length} optional token(s) not declared — library default applies`)}`);
463
- if (SHOW_OPTIONAL) for (const token of list) console.log(`${indent} ${dim(token)}`);
477
+ const groups = byFeature && typeof byFeature === 'object' ? byFeature : {};
478
+ const summary = Object.entries(groups).map(([f, ts]) => `${f} ${ts.length}`).join(' · ');
479
+ console.log(`${indent}${dim(`i ${list.length} optional token(s) not declared — library default applies${summary ? ` (${summary})` : ''}`)}`);
480
+ if (SHOW_OPTIONAL) {
481
+ for (const [f, ts] of Object.entries(groups)) {
482
+ console.log(`${indent} ${dim(f + ':')}`);
483
+ for (const token of ts) console.log(`${indent} ${dim(token)}`);
484
+ }
485
+ }
464
486
  }
465
487
 
466
488
  function reportResult(result) {
@@ -483,7 +505,7 @@ function reportResult(result) {
483
505
  console.log(
484
506
  ` ${green('✓')} [data-theme="${t.name}"] ${dim(`(${t.declaredCount} tokens)`)}`
485
507
  );
486
- optionalInfo(t.optionalMissing, ' ');
508
+ optionalInfo(t.optionalMissing, t.optionalMissingByFeature, ' ');
487
509
  } else {
488
510
  const n = t.missing.length;
489
511
  console.log(
@@ -502,7 +524,7 @@ function reportResult(result) {
502
524
  console.log(
503
525
  `${green('✓')} ${bold(rel)} ${dim(`passes (${result.declaredCount} tokens declared)`)}`
504
526
  );
505
- optionalInfo(result.optionalMissing, ' ');
527
+ optionalInfo(result.optionalMissing, result.optionalMissingByFeature, ' ');
506
528
  return;
507
529
  }
508
530
 
@@ -583,7 +605,7 @@ function printUsage() {
583
605
  ' --all validate every theme.css under public/ (CI mode)',
584
606
  ' --no-a11y skip the WCAG 2.2 AA contrast audit',
585
607
  ' --allow-a11y-fail do NOT exit non-zero on a11y FAILs (report only)',
586
- ' --show-optional list optional contract tokens a theme leaves to the library default (info only)',
608
+ ' --show-optional list optional contract tokens a theme leaves to the library default, grouped by feature (info only)',
587
609
  ' --strict accepted for backwards compatibility (no-op; FAIL is now the default)',
588
610
  '',
589
611
  'Exit codes:',