css-is-awesome 1.11.0 → 1.12.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CONTRACT.md +15 -0
  3. package/README.md +9 -8
  4. package/THEMING.md +1 -0
  5. package/bin/analyze.cjs +88 -1
  6. package/bin/cia.cjs +30 -5
  7. package/bin/migrate-chakra.cjs +249 -0
  8. package/bin/migrate-mui.cjs +363 -0
  9. package/bin/migrate-tailwind.cjs +3 -1
  10. package/dist/css-is-awesome.css +516 -0
  11. package/dist/css-is-awesome.min.css +1 -1
  12. package/dist/css-is-awesome.utilities.css +516 -0
  13. package/dist/css-is-awesome.utilities.min.css +1 -1
  14. package/dist/tokens.d.ts +4 -2
  15. package/package.json +7 -3
  16. package/public/theme.css +293 -237
  17. package/public/themes/boilerplate/theme.css +11 -10
  18. package/public/themes/boilerplate-dark/theme.css +10 -9
  19. package/public/themes/boilerplate-light/theme.css +10 -9
  20. package/public/themes/cupertino/theme.css +11 -10
  21. package/public/themes/cupertino-dark/theme.css +11 -10
  22. package/public/themes/cupertino-light/theme.css +11 -10
  23. package/public/themes/glass/theme.css +11 -10
  24. package/public/themes/glass-dark/theme.css +11 -10
  25. package/public/themes/glass-light/theme.css +11 -10
  26. package/public/themes/graphite/theme.css +11 -10
  27. package/public/themes/graphite-dark/theme.css +11 -10
  28. package/public/themes/graphite-light/theme.css +11 -10
  29. package/public/themes/press/theme.css +11 -10
  30. package/public/themes/press-dark/theme.css +11 -10
  31. package/public/themes/press-light/theme.css +11 -10
  32. package/public/themes/prism/theme.css +11 -10
  33. package/public/themes/prism-dark/theme.css +11 -10
  34. package/public/themes/prism-light/theme.css +11 -10
  35. package/public/themes/sketchbook/theme.css +11 -10
  36. package/public/themes/sketchbook-dark/theme.css +11 -10
  37. package/public/themes/sketchbook-light/theme.css +10 -9
  38. package/public/themes/terminal/theme.css +11 -10
  39. package/public/themes/terminal-dark/theme.css +11 -10
  40. package/public/themes/terminal-light/theme.css +11 -10
  41. package/scripts/theme-a11y.js +11 -31
  42. package/scripts/theme-contract.json +1 -0
  43. package/scss/_spacing-scale.scss +25 -0
  44. package/scss/_utilities.scss +44 -9
  45. package/scss/components/_accordion.scss +1 -1
  46. package/scss/components/_data.scss +2 -2
  47. package/scss/components/_feedback.scss +1 -1
  48. package/scss/components/_forms.scss +16 -2
  49. package/scss/components/_navigation.scss +1 -1
  50. package/scss/examples/_usage.scss +3 -3
  51. package/scss/recipes/admin-dashboard-layout.md +277 -0
  52. package/scss/recipes/app-shell.md +339 -0
  53. package/scss/recipes/auth-flow.md +345 -0
  54. package/scss/recipes/confirm-dialog.md +248 -0
  55. package/scss/recipes/data-table.md +382 -0
  56. package/scss/recipes/datepicker.md +428 -0
  57. package/scss/recipes/form-validation-async.md +340 -0
  58. package/scss/recipes/form-validation-html5.md +306 -0
  59. package/scss/recipes/form-validation-react-hook-form.md +224 -0
  60. package/scss/recipes/form-validation-success-states.md +290 -0
  61. package/scss/recipes/form-validation-zod.md +270 -0
  62. package/scss/recipes/i18n-date-formatting.md +172 -0
  63. package/scss/recipes/i18n-number-currency.md +145 -0
  64. package/scss/recipes/i18n-pluralization.md +163 -0
  65. package/scss/recipes/multi-step-wizard.md +365 -0
  66. package/scss/recipes/otp-input.md +310 -0
  67. package/scss/recipes/rtl-layout.md +273 -0
  68. package/scss/themes/boilerplate-dark.scss +2 -10
  69. package/scss/themes/boilerplate-light.scss +2 -10
  70. package/scss/themes/boilerplate.scss +2 -10
  71. package/scss/themes/cupertino-dark.scss +2 -10
  72. package/scss/themes/cupertino-light.scss +2 -10
  73. package/scss/themes/cupertino.scss +2 -10
  74. package/scss/themes/glass-dark.scss +2 -10
  75. package/scss/themes/glass-light.scss +2 -10
  76. package/scss/themes/glass.scss +2 -10
  77. package/scss/themes/graphite-dark.scss +2 -10
  78. package/scss/themes/graphite-light.scss +2 -10
  79. package/scss/themes/graphite.scss +2 -10
  80. package/scss/themes/press-dark.scss +2 -10
  81. package/scss/themes/press-light.scss +2 -10
  82. package/scss/themes/press.scss +2 -10
  83. package/scss/themes/prism-dark.scss +2 -10
  84. package/scss/themes/prism-light.scss +2 -10
  85. package/scss/themes/prism.scss +2 -10
  86. package/scss/themes/sketchbook-dark.scss +2 -10
  87. package/scss/themes/sketchbook-light.scss +2 -10
  88. package/scss/themes/sketchbook.scss +2 -10
  89. package/scss/themes/terminal-dark.scss +2 -10
  90. package/scss/themes/terminal-light.scss +2 -10
  91. package/scss/themes/terminal.scss +2 -10
@@ -0,0 +1,363 @@
1
+ /**
2
+ * cia migrate mui — parse a MUI (Material UI, v5+) theme object + map to cia.
3
+ *
4
+ * v1.2 EPIC-05. Same shape as migrate-tailwind / migrate-bootstrap:
5
+ * 1. Load the consumer's theme module (createTheme({...}) result, or a
6
+ * plain theme-shaped object)
7
+ * 2. Map to cia contract tokens with confidence levels, reusing the shared
8
+ * mapping engine from migrate-tailwind.cjs
9
+ * 3. Write a cia theme.scss (via writeThemeScss)
10
+ *
11
+ * Token-only: this reads MUI's theme VALUES (colors, spacing, radius, font
12
+ * family). It never imports @mui/material and never translates MUI React
13
+ * components — cia has no component library to translate them to. Out of
14
+ * scope, same reason Tailwind/Bootstrap migration doesn't do it either.
15
+ */
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // Shared engine from the Tailwind module — no need to fork it.
22
+ const {
23
+ loadConfig,
24
+ writeThemeScss,
25
+ parseFlags,
26
+ closestByRem,
27
+ CIA_SPACING,
28
+ CIA_RADII,
29
+ } = require('./migrate-tailwind.cjs');
30
+
31
+ // ─── theme object discovery ───────────────────────────────────────────────
32
+
33
+ // createTheme({...}) already returns a fully-resolved plain object — no MUI
34
+ // import needed to read one. Accept a few common export shapes.
35
+ function findMuiTheme(mod) {
36
+ if (mod && typeof mod === 'object') {
37
+ if (mod.theme && mod.theme.palette) return mod.theme;
38
+ if (mod.default && mod.default.palette) return mod.default;
39
+ if (mod.palette) return mod;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // ─── color mapping ─────────────────────────────────────────────────────────
45
+
46
+ const PALETTE_GROUPS = [
47
+ ['primary', 'action-primary-default'],
48
+ ['secondary', 'action-secondary-default'],
49
+ ['error', 'error-default'],
50
+ ['warning', 'warning-default'],
51
+ ['info', 'info-default'],
52
+ ['success', 'success-default'],
53
+ ];
54
+
55
+ // Deliberately maps ONLY `.main` for each group — writeThemeScss already
56
+ // auto-derives hover/active via m.states() once a `<group>-default` token
57
+ // is set (same as Tailwind/Bootstrap get). Also mapping MUI's own .dark/
58
+ // .light shades would create two competing hover mechanisms in one theme.
59
+ function mapMuiColors(palette) {
60
+ const mappings = {};
61
+ const unmapped = [];
62
+ if (!palette || typeof palette !== 'object') return { mappings, unmapped };
63
+
64
+ for (const [muiKey, ciaToken] of PALETTE_GROUPS) {
65
+ const group = palette[muiKey];
66
+ if (group && typeof group.main === 'string') {
67
+ mappings[ciaToken] = {
68
+ value: group.main,
69
+ source: `palette.${muiKey}.main`,
70
+ confidence: 'HIGH',
71
+ rationale: `MUI 'palette.${muiKey}.main' → cia '${ciaToken}' (direct semantic match; hover/active derive automatically via m.states()).`,
72
+ };
73
+ }
74
+ }
75
+
76
+ if (palette.text && typeof palette.text.primary === 'string') {
77
+ mappings['text-primary'] = {
78
+ value: palette.text.primary,
79
+ source: 'palette.text.primary',
80
+ confidence: 'HIGH',
81
+ rationale: `MUI 'palette.text.primary' → cia 'text-primary'.`,
82
+ };
83
+ }
84
+ if (palette.text && typeof palette.text.secondary === 'string') {
85
+ mappings['text-secondary'] = {
86
+ value: palette.text.secondary,
87
+ source: 'palette.text.secondary',
88
+ confidence: 'HIGH',
89
+ rationale: `MUI 'palette.text.secondary' → cia 'text-secondary'.`,
90
+ };
91
+ }
92
+ if (palette.background && typeof palette.background.default === 'string') {
93
+ mappings['background-default'] = {
94
+ value: palette.background.default,
95
+ source: 'palette.background.default',
96
+ confidence: 'HIGH',
97
+ rationale: `MUI 'palette.background.default' → cia 'background-default'.`,
98
+ };
99
+ }
100
+ if (palette.background && typeof palette.background.paper === 'string') {
101
+ mappings['surface-default'] = {
102
+ value: palette.background.paper,
103
+ source: 'palette.background.paper',
104
+ confidence: 'MEDIUM',
105
+ rationale: `MUI 'palette.background.paper' (its elevated-surface concept) → cia 'surface-default'.`,
106
+ };
107
+ }
108
+
109
+ return { mappings, unmapped };
110
+ }
111
+
112
+ // ─── spacing mapping ─────────────────────────────────────────────────────
113
+
114
+ // MUI's `spacing` is a number (px base unit, default 8), a function
115
+ // `(factor) => value`, or an array of explicit values. Sample factors 1-9
116
+ // (matching cia's own 9-step scale) through whichever form it is.
117
+ function sampleMuiSpacing(spacing) {
118
+ const samples = {};
119
+ const factors = [1, 2, 3, 4, 5, 6, 7, 8, 9];
120
+
121
+ if (typeof spacing === 'number') {
122
+ for (const f of factors) samples[f] = (spacing * f) / 16; // px -> rem
123
+ return samples;
124
+ }
125
+ if (Array.isArray(spacing)) {
126
+ for (const f of factors) {
127
+ const v = spacing[f];
128
+ if (v == null) continue;
129
+ const n = typeof v === 'number' ? v / 16 : parseFloat(v);
130
+ if (!Number.isNaN(n)) samples[f] = n;
131
+ }
132
+ return samples;
133
+ }
134
+ if (typeof spacing === 'function') {
135
+ for (const f of factors) {
136
+ try {
137
+ const v = spacing(f);
138
+ const n = typeof v === 'number' ? v / 16 : parseFloat(v);
139
+ if (!Number.isNaN(n)) samples[f] = n;
140
+ } catch {
141
+ // consumer's spacing() referenced something outside this sandbox — skip
142
+ }
143
+ }
144
+ return samples;
145
+ }
146
+ return samples;
147
+ }
148
+
149
+ function mapMuiSpacing(spacing) {
150
+ const mappings = {};
151
+ const unmapped = [];
152
+ const samples = sampleMuiSpacing(spacing);
153
+ const claimedSlots = {};
154
+
155
+ const sorted = Object.entries(samples)
156
+ .map(([factor, rem]) => ({ factor, rem, match: closestByRem(rem, CIA_SPACING) }))
157
+ .sort((a, b) => (a.match ? a.match.deltaRem : Infinity) - (b.match ? b.match.deltaRem : Infinity));
158
+
159
+ for (const { factor, rem, match } of sorted) {
160
+ if (!match) {
161
+ unmapped.push({ source: `spacing(${factor})`, value: `${rem}rem`, reason: `No cia spacing slot within ±0.25rem of ${rem}rem.` });
162
+ continue;
163
+ }
164
+ const slot = `space-${match.key}`;
165
+ if (claimedSlots[slot]) continue; // closer factor already claimed this slot
166
+ claimedSlots[slot] = factor;
167
+ mappings[slot] = {
168
+ value: `${match.value}rem`,
169
+ source: `spacing(${factor})`,
170
+ confidence: match.confidence,
171
+ rationale: match.confidence === 'HIGH'
172
+ ? `MUI 'spacing(${factor})' (${rem}rem) is an exact match for cia ${slot}.`
173
+ : `Closest cia spacing slot to MUI 'spacing(${factor})' (${rem}rem) is ${slot} (${match.value}rem); delta ${match.deltaRem.toFixed(4)}rem.`,
174
+ };
175
+ }
176
+ return { mappings, unmapped };
177
+ }
178
+
179
+ // ─── master mapping ─────────────────────────────────────────────────────────
180
+
181
+ function mapMuiToCia(theme) {
182
+ const allMappings = {};
183
+ const allUnmapped = [];
184
+
185
+ const colors = mapMuiColors(theme.palette);
186
+ Object.assign(allMappings, colors.mappings);
187
+ allUnmapped.push(...colors.unmapped);
188
+
189
+ if (theme.spacing != null) {
190
+ const spacing = mapMuiSpacing(theme.spacing);
191
+ Object.assign(allMappings, spacing.mappings);
192
+ allUnmapped.push(...spacing.unmapped);
193
+ }
194
+
195
+ if (theme.shape && typeof theme.shape.borderRadius === 'number') {
196
+ const rem = theme.shape.borderRadius / 16;
197
+ const match = closestByRem(rem, CIA_RADII);
198
+ if (match && match.key !== 'full' && match.key !== 'none') {
199
+ allMappings[`radius-${match.key}`] = {
200
+ value: `${match.value}rem`,
201
+ source: 'shape.borderRadius',
202
+ confidence: match.confidence,
203
+ rationale: `MUI 'shape.borderRadius' (${theme.shape.borderRadius}px) is closest to cia radius-${match.key} (${match.value}rem).`,
204
+ };
205
+ } else {
206
+ allUnmapped.push({ source: 'shape.borderRadius', value: `${theme.shape.borderRadius}px`, reason: 'No cia radius slot within ±0.25rem.' });
207
+ }
208
+ }
209
+
210
+ if (theme.typography && typeof theme.typography.fontFamily === 'string') {
211
+ allMappings['font-sans'] = {
212
+ value: theme.typography.fontFamily,
213
+ source: 'typography.fontFamily',
214
+ confidence: 'HIGH',
215
+ rationale: `MUI 'typography.fontFamily' → cia 'font-sans' (direct stack copy).`,
216
+ };
217
+ }
218
+
219
+ // Deliberately not mapped — see the epic's scope note: MUI's 24-elevation
220
+ // shadow system has no natural cia analog (cia has 5 named shadow steps),
221
+ // and per-heading typography sizes / breakpoints are a long tail with no
222
+ // single-value confidence story. Surfaced once, informationally, rather
223
+ // than forcing an arbitrary per-field mapping.
224
+ if (Array.isArray(theme.shadows) && theme.shadows.some((s) => s && s !== 'none')) {
225
+ allUnmapped.push({
226
+ source: 'shadows[0..24]',
227
+ value: `${theme.shadows.length} elevation levels`,
228
+ reason: `MUI's 24-elevation shadow system has no 1:1 cia analog (cia has 5 named steps: shadow-sm/md/lg/xl/2xl). Review and pick manually — mapping automatically would be arbitrary, not more correct.`,
229
+ });
230
+ }
231
+ if (theme.breakpoints && theme.breakpoints.values) {
232
+ allUnmapped.push({
233
+ source: 'breakpoints.values',
234
+ value: JSON.stringify(theme.breakpoints.values),
235
+ reason: `cia's breakpoints are a fixed system scale (see scss/_system.scss), not themed per-project — not migrated. Note for reference only.`,
236
+ });
237
+ }
238
+
239
+ const counts = { HIGH: 0, MEDIUM: 0, LOW: 0, UNMAPPED: 0 };
240
+ for (const m of Object.values(allMappings)) {
241
+ if (counts[m.confidence] != null) counts[m.confidence] += 1;
242
+ }
243
+ counts.UNMAPPED = allUnmapped.length;
244
+ counts.total = counts.HIGH + counts.MEDIUM + counts.LOW + counts.UNMAPPED;
245
+
246
+ return { mappings: allMappings, unmapped: allUnmapped, report: counts };
247
+ }
248
+
249
+ // ─── CLI entry ────────────────────────────────────────────────────────────
250
+
251
+ const HELP = `cia migrate mui — convert a MUI (Material UI v5+) theme object to a cia theme.scss
252
+
253
+ Usage:
254
+ cia migrate mui <path> [options]
255
+
256
+ Default behavior:
257
+ Loads a module exporting a MUI theme (the result of createTheme({...}),
258
+ as a default export, a named 'theme' export, or a plain theme-shaped
259
+ object), maps its palette/spacing/shape/typography to cia contract
260
+ tokens with confidence scoring, and writes a cia theme.scss to
261
+ ./cia-themes/<name>.scss. Diagnostics print to stderr.
262
+
263
+ Token-only: never imports @mui/material, never translates MUI React
264
+ components — only reads plain values off the theme object.
265
+
266
+ Arguments:
267
+ path Path to a .js/.ts/.mjs/.cjs module exporting a MUI theme.
268
+
269
+ Options:
270
+ --name <name> Theme name. Default: migrated
271
+ --out <path> Custom output path. Default: ./cia-themes/<name>.scss
272
+ --json Skip the file write and dump JSON to stdout (pipe-safe).
273
+ -h, --help Show this help.
274
+
275
+ Examples:
276
+ cia migrate mui ./src/theme.ts
277
+ cia migrate mui ./src/theme.ts --name acme
278
+ cia migrate mui ./src/theme.ts --json | jq '.cia.report'
279
+
280
+ Notes:
281
+ - MUI's 24-elevation shadow system and per-breakpoint values are not
282
+ mapped automatically — see the UNMAPPED block for why.
283
+ - Hover/active states are NOT read from palette.<group>.dark/.light —
284
+ cia derives them automatically from the -default value via m.states().
285
+ `;
286
+
287
+ async function run(args) {
288
+ const { flags, positional } = parseFlags(args);
289
+ if (flags.help) {
290
+ process.stdout.write(HELP);
291
+ return;
292
+ }
293
+
294
+ let themePath = positional[0];
295
+ if (!themePath) {
296
+ throw new Error(`cia migrate mui requires a path to a module exporting a MUI theme. Example: cia migrate mui ./src/theme.ts`);
297
+ }
298
+ if (!path.isAbsolute(themePath)) themePath = path.resolve(process.cwd(), themePath);
299
+ if (!fs.existsSync(themePath)) throw new Error(`theme file not found: ${themePath}`);
300
+
301
+ process.stderr.write(`cia migrate mui\n`);
302
+ process.stderr.write(` theme: ${themePath}\n`);
303
+ process.stderr.write(` loading...\n`);
304
+
305
+ const mod = loadConfig(themePath);
306
+ const theme = findMuiTheme(mod);
307
+ if (!theme) {
308
+ throw new Error(
309
+ `Could not find a MUI theme in ${themePath}. Expected a default export, a named ` +
310
+ `'theme' export, or the module itself to be a theme-shaped object with a 'palette' key ` +
311
+ `(the result of createTheme({...})).`,
312
+ );
313
+ }
314
+
315
+ process.stderr.write(` found theme (mode: ${theme.palette?.mode ?? 'light'})\n`);
316
+ process.stderr.write(`\n mapping to cia contract tokens...\n`);
317
+ const cia = mapMuiToCia(theme);
318
+
319
+ process.stderr.write(`\n ─── confidence report ─────────────────────────\n`);
320
+ process.stderr.write(` HIGH (exact match): ${cia.report.HIGH}\n`);
321
+ process.stderr.write(` MEDIUM (close, ≤0.125rem): ${cia.report.MEDIUM}\n`);
322
+ process.stderr.write(` LOW (best guess): ${cia.report.LOW}\n`);
323
+ process.stderr.write(` UNMAPPED (no cia analog): ${cia.report.UNMAPPED}\n`);
324
+ process.stderr.write(` ─────────────────────────────────────────────\n`);
325
+ process.stderr.write(` total ${cia.report.total}\n`);
326
+
327
+ if (flags.json) {
328
+ process.stderr.write(`\n --- JSON dump on stdout below ---\n`);
329
+ process.stdout.write(JSON.stringify({ source: themePath, cia }, null, 2));
330
+ process.stdout.write('\n');
331
+ return;
332
+ }
333
+
334
+ const scss = writeThemeScss(cia, { name: flags.name, source: themePath, tool: 'mui' });
335
+ let outPath = flags.out;
336
+ if (outPath) {
337
+ if (!path.isAbsolute(outPath)) outPath = path.resolve(process.cwd(), outPath);
338
+ } else {
339
+ outPath = path.join(process.cwd(), 'cia-themes', `${flags.name}.scss`);
340
+ }
341
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
342
+ fs.writeFileSync(outPath, scss, 'utf8');
343
+
344
+ process.stderr.write(`\n wrote ${outPath}\n`);
345
+ process.stderr.write(` ${scss.split('\n').length - 1} lines, ${Object.keys(cia.mappings).length} mapped tokens\n`);
346
+ if (cia.report.UNMAPPED > 0) {
347
+ process.stderr.write(` ${cia.report.UNMAPPED} unmapped/informational entries appended as a comment block — review\n`);
348
+ }
349
+ process.stderr.write(`\n Next steps:\n`);
350
+ process.stderr.write(` 1. Review the file (MEDIUM/LOW/UNMAPPED entries are flagged)\n`);
351
+ process.stderr.write(` 2. @use this file from your app's SCSS entry\n`);
352
+ process.stderr.write(` 3. Set <html data-theme="${flags.name}"> and run npm run build:css\n`);
353
+ process.stderr.write(` 4. Run npm run validate-themes to confirm WCAG 2.2 AA contrast\n`);
354
+ }
355
+
356
+ module.exports = {
357
+ run,
358
+ findMuiTheme,
359
+ mapMuiColors,
360
+ mapMuiSpacing,
361
+ mapMuiToCia,
362
+ sampleMuiSpacing,
363
+ };
@@ -586,7 +586,8 @@ function writeThemeScss(cia, options = {}) {
586
586
  timestamp = new Date().toISOString().slice(0, 19) + 'Z',
587
587
  } = options;
588
588
 
589
- const sourceLabel = tool === 'bootstrap' ? 'Bootstrap values' : 'Tailwind values';
589
+ const TOOL_LABELS = { bootstrap: 'Bootstrap values', mui: 'MUI values', chakra: 'Chakra values', tailwind: 'Tailwind values' };
590
+ const sourceLabel = TOOL_LABELS[tool] || 'Tailwind values';
590
591
  const lines = [];
591
592
 
592
593
  // ── Header (block comment so consumer sees it before any SCSS) ──
@@ -812,6 +813,7 @@ async function run(args) {
812
813
  module.exports = {
813
814
  run,
814
815
  findTailwindConfig,
816
+ loadConfig,
815
817
  extractTheme,
816
818
  summary,
817
819
  // Mapping internals exported for unit testing + external introspection