css-is-awesome 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/AGENTS.md +343 -0
  2. package/CHANGELOG.md +1036 -0
  3. package/CLAUDE.md +9 -0
  4. package/CONTRACT.md +580 -0
  5. package/GEMINI.md +9 -0
  6. package/LICENSE +21 -0
  7. package/LICENSE-third-party +88 -0
  8. package/MIGRATION.md +312 -0
  9. package/README.md +380 -0
  10. package/ROADMAP.md +714 -0
  11. package/THEMING.md +290 -0
  12. package/THREE-TIERS.md +158 -0
  13. package/VERSIONING.md +200 -0
  14. package/_index.scss +34 -0
  15. package/api.scss +16 -0
  16. package/bin/README.md +57 -0
  17. package/bin/cia.cjs +114 -0
  18. package/bin/migrate-bootstrap.cjs +427 -0
  19. package/bin/migrate-tailwind.cjs +832 -0
  20. package/css-is-awesome.instructions.md +533 -0
  21. package/dist/css-is-awesome.core.css +598 -0
  22. package/dist/css-is-awesome.core.min.css +1 -0
  23. package/dist/css-is-awesome.css +3733 -0
  24. package/dist/css-is-awesome.min.css +1 -0
  25. package/dist/css-is-awesome.utilities.css +2445 -0
  26. package/dist/css-is-awesome.utilities.min.css +1 -0
  27. package/dist/tokens.css +508 -0
  28. package/dist/tokens.d.ts +266 -0
  29. package/dist/tokens.min.css +1 -0
  30. package/figma-tokens/README.md +57 -0
  31. package/figma-tokens/primitives-brand.scss +121 -0
  32. package/figma-tokens/semantic-dark.scss +61 -0
  33. package/figma-tokens/semantic-light.scss +61 -0
  34. package/figma-tokens/tokens.json +144 -0
  35. package/llm.txt +183 -0
  36. package/mcp/server.cjs +1495 -0
  37. package/package.json +199 -0
  38. package/public/icons/README.md +278 -0
  39. package/public/icons/arrow-right.svg +4 -0
  40. package/public/icons/check.svg +4 -0
  41. package/public/icons/chevron-down.svg +4 -0
  42. package/public/icons/close.svg +4 -0
  43. package/public/icons/core/arrow-down.svg +13 -0
  44. package/public/icons/core/arrow-left.svg +13 -0
  45. package/public/icons/core/arrow-right.svg +13 -0
  46. package/public/icons/core/arrow-up.svg +13 -0
  47. package/public/icons/core/bell.svg +13 -0
  48. package/public/icons/core/calendar.svg +15 -0
  49. package/public/icons/core/check.svg +12 -0
  50. package/public/icons/core/chevron-down.svg +12 -0
  51. package/public/icons/core/chevron-left.svg +12 -0
  52. package/public/icons/core/chevron-right.svg +12 -0
  53. package/public/icons/core/chevron-up.svg +12 -0
  54. package/public/icons/core/clock.svg +13 -0
  55. package/public/icons/core/close.svg +13 -0
  56. package/public/icons/core/copy.svg +13 -0
  57. package/public/icons/core/download.svg +14 -0
  58. package/public/icons/core/edit.svg +13 -0
  59. package/public/icons/core/error.svg +14 -0
  60. package/public/icons/core/external-link.svg +14 -0
  61. package/public/icons/core/eye-off.svg +15 -0
  62. package/public/icons/core/eye.svg +13 -0
  63. package/public/icons/core/filter.svg +12 -0
  64. package/public/icons/core/heart.svg +12 -0
  65. package/public/icons/core/help.svg +14 -0
  66. package/public/icons/core/home.svg +13 -0
  67. package/public/icons/core/info.svg +14 -0
  68. package/public/icons/core/loading.svg +12 -0
  69. package/public/icons/core/lock.svg +13 -0
  70. package/public/icons/core/mail.svg +13 -0
  71. package/public/icons/core/menu.svg +14 -0
  72. package/public/icons/core/message.svg +12 -0
  73. package/public/icons/core/minus.svg +12 -0
  74. package/public/icons/core/more-horizontal.svg +14 -0
  75. package/public/icons/core/pause.svg +13 -0
  76. package/public/icons/core/play.svg +12 -0
  77. package/public/icons/core/plus.svg +13 -0
  78. package/public/icons/core/refresh.svg +15 -0
  79. package/public/icons/core/save.svg +14 -0
  80. package/public/icons/core/search.svg +13 -0
  81. package/public/icons/core/settings.svg +13 -0
  82. package/public/icons/core/share.svg +14 -0
  83. package/public/icons/core/sort.svg +15 -0
  84. package/public/icons/core/star.svg +12 -0
  85. package/public/icons/core/success.svg +13 -0
  86. package/public/icons/core/trash.svg +16 -0
  87. package/public/icons/core/unlock.svg +13 -0
  88. package/public/icons/core/upload.svg +14 -0
  89. package/public/icons/core/user.svg +13 -0
  90. package/public/icons/core/users.svg +15 -0
  91. package/public/icons/core/warning.svg +14 -0
  92. package/public/icons/download.svg +4 -0
  93. package/public/icons/edit.svg +4 -0
  94. package/public/icons/menu.svg +4 -0
  95. package/public/icons/search.svg +4 -0
  96. package/public/theme.css +4179 -0
  97. package/public/themes/README.md +102 -0
  98. package/public/themes/boilerplate/theme.css +154 -0
  99. package/public/themes/boilerplate-dark/theme.css +171 -0
  100. package/public/themes/boilerplate-light/theme.css +171 -0
  101. package/public/themes/cupertino/theme.css +165 -0
  102. package/public/themes/cupertino-dark/theme.css +200 -0
  103. package/public/themes/cupertino-light/theme.css +227 -0
  104. package/public/themes/glass/theme.css +190 -0
  105. package/public/themes/glass-dark/theme.css +201 -0
  106. package/public/themes/glass-light/theme.css +229 -0
  107. package/public/themes/graphite/theme.css +155 -0
  108. package/public/themes/graphite-dark/theme.css +223 -0
  109. package/public/themes/graphite-light/theme.css +191 -0
  110. package/public/themes/press/theme.css +189 -0
  111. package/public/themes/press-dark/theme.css +199 -0
  112. package/public/themes/press-light/theme.css +235 -0
  113. package/public/themes/prism/theme.css +159 -0
  114. package/public/themes/prism-dark/theme.css +185 -0
  115. package/public/themes/prism-light/theme.css +188 -0
  116. package/public/themes/sketchbook/theme.css +152 -0
  117. package/public/themes/sketchbook-dark/theme.css +194 -0
  118. package/public/themes/sketchbook-light/theme.css +188 -0
  119. package/public/themes/terminal/theme.css +159 -0
  120. package/public/themes/terminal-dark/theme.css +229 -0
  121. package/public/themes/terminal-light/theme.css +152 -0
  122. package/scripts/README.md +70 -0
  123. package/scripts/icon-contract.json +67 -0
  124. package/scripts/icon-validator.js +205 -0
  125. package/scripts/prepare-dist.mjs +65 -0
  126. package/scripts/theme-a11y.js +415 -0
  127. package/scripts/theme-contract.json +171 -0
  128. package/scripts/theme-validator.js +674 -0
  129. package/scss/README.md +55 -0
  130. package/scss/_animations-utilities.scss +107 -0
  131. package/scss/_animations.scss +248 -0
  132. package/scss/_app-styles.scss +15 -0
  133. package/scss/_generator.scss +174 -0
  134. package/scss/_icons.scss +475 -0
  135. package/scss/_index.scss +17 -0
  136. package/scss/_layout.scss +597 -0
  137. package/scss/_mixins.scss +1036 -0
  138. package/scss/_system.scss +291 -0
  139. package/scss/_utilities.scss +377 -0
  140. package/scss/api.scss +61 -0
  141. package/scss/components/_accordion.scss +72 -0
  142. package/scss/components/_buttons.scss +171 -0
  143. package/scss/components/_copy-button.scss +69 -0
  144. package/scss/components/_data.scss +190 -0
  145. package/scss/components/_feedback.scss +197 -0
  146. package/scss/components/_forms.scss +272 -0
  147. package/scss/components/_index.scss +17 -0
  148. package/scss/components/_navigation.scss +186 -0
  149. package/scss/components/_overlay.scss +259 -0
  150. package/scss/components/_stepper.scss +159 -0
  151. package/scss/components/_tabs.scss +72 -0
  152. package/scss/core.scss +54 -0
  153. package/scss/examples/_theming.scss +107 -0
  154. package/scss/examples/_usage.scss +317 -0
  155. package/scss/main.scss +64 -0
  156. package/scss/recipes/README.md +110 -0
  157. package/scss/recipes/_bare-tags.scss +232 -0
  158. package/scss/recipes/_recipe-template.md +119 -0
  159. package/scss/recipes/combobox.md +509 -0
  160. package/scss/recipes/dialog.md +265 -0
  161. package/scss/recipes/print-to-pdf.md +263 -0
  162. package/scss/theme/README.md +67 -0
  163. package/scss/theme/_brand.scss +18 -0
  164. package/scss/theme/_colors-dark.scss +68 -0
  165. package/scss/theme/_colors-light.scss +70 -0
  166. package/scss/theme/_components.scss +94 -0
  167. package/scss/theme/_icons.scss +104 -0
  168. package/scss/theme/_index.scss +62 -0
  169. package/scss/theme/_shadows.scss +65 -0
  170. package/scss/theme/_shape.scss +13 -0
  171. package/scss/theme/_typography.scss +9 -0
  172. package/scss/theme/registry.json +447 -0
  173. package/scss/themes/boilerplate-dark.scss +212 -0
  174. package/scss/themes/boilerplate-light.scss +209 -0
  175. package/scss/themes/boilerplate.scss +181 -0
  176. package/scss/themes/cupertino-dark.scss +238 -0
  177. package/scss/themes/cupertino-light.scss +266 -0
  178. package/scss/themes/cupertino.scss +194 -0
  179. package/scss/themes/glass-dark.scss +238 -0
  180. package/scss/themes/glass-light.scss +266 -0
  181. package/scss/themes/glass.scss +220 -0
  182. package/scss/themes/graphite-dark.scss +260 -0
  183. package/scss/themes/graphite-light.scss +228 -0
  184. package/scss/themes/graphite.scss +182 -0
  185. package/scss/themes/press-dark.scss +237 -0
  186. package/scss/themes/press-light.scss +273 -0
  187. package/scss/themes/press.scss +241 -0
  188. package/scss/themes/prism-dark.scss +217 -0
  189. package/scss/themes/prism-light.scss +220 -0
  190. package/scss/themes/prism.scss +190 -0
  191. package/scss/themes/sketchbook-dark.scss +231 -0
  192. package/scss/themes/sketchbook-light.scss +231 -0
  193. package/scss/themes/sketchbook.scss +178 -0
  194. package/scss/themes/terminal-dark.scss +267 -0
  195. package/scss/themes/terminal-light.scss +175 -0
  196. package/scss/themes/terminal.scss +185 -0
  197. package/scss/tokens.scss +23 -0
  198. package/scss/utilities-only.scss +4 -0
@@ -0,0 +1,832 @@
1
+ /**
2
+ * cia migrate tailwind — extract theme from a tailwind.config.* file.
3
+ *
4
+ * Status: PR 1 — parse + dump JSON to stdout. Future PRs:
5
+ * - US-03.1.3: map Tailwind tokens to cia contract tokens (color palette,
6
+ * spacing scale, font-size matching, confidence report)
7
+ * - US-03.1.4: write the cia theme.scss file wrapped in @include cia.theme()
8
+ *
9
+ * This module is self-contained — no cia internals. Loaded lazily by the
10
+ * bin/cia.cjs router when the user invokes `cia migrate tailwind`.
11
+ */
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // Search order matches Tailwind's own resolution: .ts → .mjs → .cjs → .js.
18
+ // We walk upward from the given start dir until one match is found.
19
+ const SEARCH_NAMES = [
20
+ 'tailwind.config.ts',
21
+ 'tailwind.config.mjs',
22
+ 'tailwind.config.cjs',
23
+ 'tailwind.config.js',
24
+ ];
25
+
26
+ function findTailwindConfig(startDir = process.cwd()) {
27
+ let dir = startDir;
28
+ while (true) {
29
+ for (const name of SEARCH_NAMES) {
30
+ const p = path.join(dir, name);
31
+ if (fs.existsSync(p)) return p;
32
+ }
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir) return null;
35
+ dir = parent;
36
+ }
37
+ }
38
+
39
+ // jiti gives us .ts/.mjs support via a runtime require shim. Optional dep —
40
+ // consumers who only have .js/.cjs configs don't need it installed.
41
+ function loadConfig(absPath) {
42
+ const ext = path.extname(absPath).toLowerCase();
43
+ try {
44
+ const jiti = require('jiti')(__filename, { interopDefault: true });
45
+ return jiti(absPath);
46
+ } catch {
47
+ if (ext === '.ts' || ext === '.mjs') {
48
+ throw new Error(
49
+ `Loading ${ext} configs requires jiti. Install it in your project: ` +
50
+ `npm install -D jiti`,
51
+ );
52
+ }
53
+ // .js / .cjs work via plain require — drop cache for a clean reload.
54
+ delete require.cache[require.resolve(absPath)];
55
+ const mod = require(absPath);
56
+ return mod && mod.default ? mod.default : mod;
57
+ }
58
+ }
59
+
60
+ // Use Tailwind's resolveConfig when available so extends + plugins + defaults
61
+ // are folded in. When Tailwind isn't installed in the consumer's project,
62
+ // fall back to a manual merge: combine `theme.extend.*` into `theme.*` for
63
+ // each known field. Less complete than resolveConfig (no plugin theme
64
+ // additions, no default values for unconfigured fields) but enough to surface
65
+ // the fields the consumer actually customized.
66
+ function resolveTailwindConfig(rawConfig) {
67
+ try {
68
+ const resolveConfig = require('tailwindcss/resolveConfig');
69
+ return { resolved: resolveConfig(rawConfig), resolverUsed: 'tailwindcss/resolveConfig' };
70
+ } catch {
71
+ return { resolved: mergeExtend(rawConfig), resolverUsed: 'manual-merge (tailwindcss not installed)' };
72
+ }
73
+ }
74
+
75
+ // Merge theme.extend.<field> into theme.<field> for known fields. Honors
76
+ // theme.<field> overrides — when both define a key, theme.<field> wins
77
+ // (matches Tailwind's actual override semantics).
78
+ function mergeExtend(rawConfig) {
79
+ const theme = (rawConfig && rawConfig.theme) || {};
80
+ const extend = theme.extend || {};
81
+ const out = { ...rawConfig, theme: {} };
82
+ const fields = new Set([...Object.keys(theme), ...Object.keys(extend)]);
83
+ fields.delete('extend');
84
+ for (const field of fields) {
85
+ const base = theme[field];
86
+ const ext = extend[field];
87
+ if (base && typeof base === 'object' && ext && typeof ext === 'object') {
88
+ out.theme[field] = { ...ext, ...base }; // theme.<field> overrides extend
89
+ } else {
90
+ out.theme[field] = base !== undefined ? base : ext;
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function extractTheme(resolved) {
97
+ const theme = (resolved && resolved.theme) || {};
98
+ return {
99
+ colors: theme.colors || {},
100
+ spacing: theme.spacing || {},
101
+ fontSize: theme.fontSize || {},
102
+ fontFamily: theme.fontFamily || {},
103
+ fontWeight: theme.fontWeight || {},
104
+ lineHeight: theme.lineHeight || {},
105
+ letterSpacing: theme.letterSpacing || {},
106
+ borderRadius: theme.borderRadius || {},
107
+ boxShadow: theme.boxShadow || {},
108
+ screens: theme.screens || {},
109
+ zIndex: theme.zIndex || {},
110
+ };
111
+ }
112
+
113
+ // Count leaves in a nested theme map (Tailwind's color objects are 2 deep:
114
+ // {blue: {50: '#...', 100: '#...', ...}}).
115
+ function countLeaves(obj) {
116
+ if (!obj || typeof obj !== 'object') return obj == null ? 0 : 1;
117
+ let count = 0;
118
+ for (const v of Object.values(obj)) {
119
+ if (v && typeof v === 'object') count += countLeaves(v);
120
+ else count += 1;
121
+ }
122
+ return count;
123
+ }
124
+
125
+ function summary(theme) {
126
+ const out = {};
127
+ for (const [field, value] of Object.entries(theme)) out[field] = countLeaves(value);
128
+ return out;
129
+ }
130
+
131
+ // ─── cia contract scales (mirror scss/_system.scss + scss/theme/_shape.scss) ──
132
+
133
+ const CIA_SPACING = {
134
+ 1: 0.5, // 8px
135
+ 2: 0.75, // 12px
136
+ 3: 0.875, // 14px
137
+ 4: 1, // 16px
138
+ 5: 1.5, // 24px
139
+ 6: 2, // 32px
140
+ 7: 3, // 48px
141
+ 8: 4, // 64px
142
+ 9: 6, // 96px
143
+ };
144
+
145
+ const CIA_FONT_SIZES = {
146
+ 1: 0.75, // 12px
147
+ 2: 0.875, // 14px
148
+ 3: 1, // 16px
149
+ 4: 1.125, // 18px
150
+ 5: 1.25, // 20px
151
+ 6: 1.5, // 24px
152
+ 7: 1.875, // 30px
153
+ 8: 2.25, // 36px
154
+ 9: 3, // 48px
155
+ 10: 3.75, // 60px
156
+ };
157
+
158
+ const CIA_RADII = {
159
+ none: 0,
160
+ sm: 0.25,
161
+ md: 0.375,
162
+ lg: 0.5,
163
+ xl: 0.75,
164
+ '2xl': 1,
165
+ full: 9999, // sentinel; we match on the keyword 'full'
166
+ };
167
+
168
+ const STATUS_PALETTE_NAMES = {
169
+ // Tailwind palette name → cia status token group
170
+ red: 'error',
171
+ rose: 'error',
172
+ error: 'error',
173
+ danger: 'error',
174
+ green: 'success',
175
+ emerald: 'success',
176
+ success: 'success',
177
+ yellow: 'warning',
178
+ amber: 'warning',
179
+ warning: 'warning',
180
+ orange: 'warning',
181
+ blue: 'info',
182
+ sky: 'info',
183
+ info: 'info',
184
+ };
185
+
186
+ const PRIMARY_PALETTE_NAMES = new Set([
187
+ 'primary', 'brand', 'accent', 'theme', 'main',
188
+ ]);
189
+
190
+ // ─── rem parsing + value comparison ──────────────────────────────────────────
191
+
192
+ // Parse a Tailwind value into a numeric rem. Accepts "1rem", "16px", numbers,
193
+ // or returns null for unparseable values (variables, gradients, etc.).
194
+ function parseRem(value) {
195
+ if (value == null) return null;
196
+ if (typeof value === 'number') return value; // assume rem
197
+ if (typeof value !== 'string') return null;
198
+ const trimmed = value.trim();
199
+ if (/^-?\d*\.?\d+rem$/i.test(trimmed)) return parseFloat(trimmed);
200
+ if (/^-?\d*\.?\d+px$/i.test(trimmed)) return parseFloat(trimmed) / 16;
201
+ if (/^-?\d*\.?\d+em$/i.test(trimmed)) return parseFloat(trimmed); // treat as rem
202
+ if (/^-?\d*\.?\d+$/i.test(trimmed)) {
203
+ const n = parseFloat(trimmed);
204
+ return n === 0 ? 0 : null; // bare 0 is fine; other bare numbers are ambiguous
205
+ }
206
+ return null;
207
+ }
208
+
209
+ // Find the closest cia scale entry by rem proximity. Returns
210
+ // { key, value, deltaRem, confidence }. confidence buckets:
211
+ // HIGH = exact (delta ≤ 0.001)
212
+ // MEDIUM = close (delta ≤ 0.125rem = 2px)
213
+ // LOW = best-guess (delta ≤ 0.25rem = 4px)
214
+ // null = no acceptable match
215
+ function closestByRem(targetRem, scale) {
216
+ let best = null;
217
+ for (const [key, value] of Object.entries(scale)) {
218
+ if (typeof value !== 'number') continue;
219
+ const delta = Math.abs(value - targetRem);
220
+ if (!best || delta < best.delta) best = { key, value, delta };
221
+ }
222
+ if (!best) return null;
223
+ let confidence;
224
+ if (best.delta < 0.001) confidence = 'HIGH';
225
+ else if (best.delta <= 0.125) confidence = 'MEDIUM';
226
+ else if (best.delta <= 0.25) confidence = 'LOW';
227
+ else return null;
228
+ return { key: best.key, value: best.value, deltaRem: best.delta, confidence };
229
+ }
230
+
231
+ // ─── color palette mapping ───────────────────────────────────────────────────
232
+
233
+ // Pick a "default shade" from a numeric Tailwind palette. Convention:
234
+ // 500 is the typical default, 600 if no 500 (some palettes start at 100/950).
235
+ function defaultShade(palette) {
236
+ if (!palette || typeof palette !== 'object') return null;
237
+ const keys = Object.keys(palette);
238
+ for (const k of ['500', '600', '400', '700', 'DEFAULT']) {
239
+ if (palette[k] != null && typeof palette[k] === 'string') return { shade: k, value: palette[k] };
240
+ }
241
+ // First string value as last resort
242
+ for (const k of keys) {
243
+ if (typeof palette[k] === 'string') return { shade: k, value: palette[k] };
244
+ }
245
+ return null;
246
+ }
247
+
248
+ // Map Tailwind colors object → cia color tokens (action-primary, error, etc.).
249
+ // Returns { mappings: {...}, unmapped: [...] }.
250
+ function mapColors(twColors) {
251
+ const mappings = {};
252
+ const unmapped = [];
253
+
254
+ if (!twColors || typeof twColors !== 'object') return { mappings, unmapped };
255
+
256
+ // Track which palettes we've already consumed so we don't double-map
257
+ let primaryUsed = null;
258
+
259
+ // Pass 1: status palettes by known names
260
+ for (const [name, value] of Object.entries(twColors)) {
261
+ const status = STATUS_PALETTE_NAMES[name.toLowerCase()];
262
+ if (status && typeof value === 'object') {
263
+ const pick = defaultShade(value);
264
+ if (pick) {
265
+ mappings[`${status}-default`] = {
266
+ value: pick.value,
267
+ source: `colors.${name}.${pick.shade}`,
268
+ confidence: 'MEDIUM',
269
+ rationale: `Tailwind palette '${name}' maps to cia status '${status}'; picked the ${pick.shade} shade by convention.`,
270
+ };
271
+ }
272
+ }
273
+ }
274
+
275
+ // Pass 2: explicitly-named primary palettes
276
+ for (const name of PRIMARY_PALETTE_NAMES) {
277
+ const value = twColors[name];
278
+ if (!value) continue;
279
+ if (typeof value === 'string') {
280
+ mappings['action-primary-default'] = {
281
+ value,
282
+ source: `colors.${name}`,
283
+ confidence: 'HIGH',
284
+ rationale: `Tailwind color named '${name}' — direct map to cia action-primary-default.`,
285
+ };
286
+ primaryUsed = name;
287
+ break;
288
+ }
289
+ if (typeof value === 'object') {
290
+ const pick = defaultShade(value);
291
+ if (pick) {
292
+ mappings['action-primary-default'] = {
293
+ value: pick.value,
294
+ source: `colors.${name}.${pick.shade}`,
295
+ confidence: 'HIGH',
296
+ rationale: `Tailwind palette named '${name}' — picked the ${pick.shade} shade as cia action-primary-default.`,
297
+ };
298
+ primaryUsed = name;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+
304
+ // Pass 3: if no primary identified, pick the FIRST non-status, non-utility palette
305
+ if (!primaryUsed) {
306
+ for (const [name, value] of Object.entries(twColors)) {
307
+ if (STATUS_PALETTE_NAMES[name.toLowerCase()]) continue;
308
+ if (['white', 'black', 'transparent', 'current', 'inherit', 'gray', 'grey', 'slate', 'zinc', 'neutral', 'stone'].includes(name.toLowerCase())) continue;
309
+ if (typeof value === 'string') {
310
+ mappings['action-primary-default'] = {
311
+ value,
312
+ source: `colors.${name}`,
313
+ confidence: 'LOW',
314
+ rationale: `No 'primary'/'brand' palette found; picked '${name}' as best guess for cia action-primary-default. Review + adjust.`,
315
+ };
316
+ primaryUsed = name;
317
+ break;
318
+ }
319
+ if (typeof value === 'object') {
320
+ const pick = defaultShade(value);
321
+ if (pick) {
322
+ mappings['action-primary-default'] = {
323
+ value: pick.value,
324
+ source: `colors.${name}.${pick.shade}`,
325
+ confidence: 'LOW',
326
+ rationale: `No 'primary'/'brand' palette found; picked '${name}' (${pick.shade} shade) as best guess for cia action-primary-default. Review + adjust.`,
327
+ };
328
+ primaryUsed = name;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Collect everything we didn't map into unmapped
336
+ for (const [name, value] of Object.entries(twColors)) {
337
+ if (name === primaryUsed) continue;
338
+ if (STATUS_PALETTE_NAMES[name.toLowerCase()]) continue;
339
+ if (['white', 'black', 'transparent', 'current', 'inherit'].includes(name.toLowerCase())) continue;
340
+ if (typeof value === 'object') {
341
+ const pick = defaultShade(value);
342
+ if (pick) {
343
+ unmapped.push({
344
+ source: `colors.${name}`,
345
+ value: pick.value,
346
+ shade: pick.shade,
347
+ reason: `Tailwind palette '${name}' — no matching cia semantic token. Suggested cia override: add as a brand-* token in your theme.scss.`,
348
+ });
349
+ }
350
+ } else if (typeof value === 'string') {
351
+ unmapped.push({
352
+ source: `colors.${name}`,
353
+ value,
354
+ reason: `Tailwind color '${name}' — no matching cia semantic token. Suggested: add to brand layer.`,
355
+ });
356
+ }
357
+ }
358
+
359
+ return { mappings, unmapped };
360
+ }
361
+
362
+ // ─── flat scale mapping (spacing / fontSize) ─────────────────────────────────
363
+
364
+ // Map each Tailwind entry to the closest cia scale step. Each Tailwind key
365
+ // either picks a cia step (with confidence) or goes to UNMAPPED.
366
+ function mapFlatScale(twValues, ciaScale, ciaTokenPrefix, kind) {
367
+ const mappings = {};
368
+ const unmapped = [];
369
+ if (!twValues || typeof twValues !== 'object') return { mappings, unmapped };
370
+
371
+ // Each Tailwind step picks the closest cia slot. Multiple Tailwind steps can
372
+ // map to the same cia slot — only the closest wins (the rest fall into
373
+ // unmapped/competing).
374
+ const claimedSlots = {};
375
+
376
+ // Sort Tailwind entries by closeness to cia so we resolve "best" first
377
+ const sorted = Object.entries(twValues)
378
+ .map(([twKey, twVal]) => {
379
+ const rem = parseRem(twVal);
380
+ const match = rem == null ? null : closestByRem(rem, ciaScale);
381
+ return { twKey, twVal, rem, match };
382
+ })
383
+ .sort((a, b) => {
384
+ const ad = a.match ? a.match.deltaRem : Infinity;
385
+ const bd = b.match ? b.match.deltaRem : Infinity;
386
+ return ad - bd;
387
+ });
388
+
389
+ for (const { twKey, twVal, rem, match } of sorted) {
390
+ if (rem == null) {
391
+ unmapped.push({
392
+ source: `${kind}.${twKey}`,
393
+ value: twVal,
394
+ reason: `Could not parse '${twVal}' as a rem/px value (variable, expression, or non-length).`,
395
+ });
396
+ continue;
397
+ }
398
+ if (!match) {
399
+ unmapped.push({
400
+ source: `${kind}.${twKey}`,
401
+ value: twVal,
402
+ reason: `No cia ${kind} slot within ±0.25rem of ${rem}rem.`,
403
+ });
404
+ continue;
405
+ }
406
+ const slot = `${ciaTokenPrefix}-${match.key}`;
407
+ if (claimedSlots[slot]) {
408
+ // Another Tailwind key already claimed this slot more closely — record
409
+ // this one as competing/unmapped
410
+ unmapped.push({
411
+ source: `${kind}.${twKey}`,
412
+ value: twVal,
413
+ reason: `Closest cia slot (${slot} = ${match.value}rem) was already taken by '${claimedSlots[slot]}'. Consider extending the cia ${kind} scale or overriding the slot.`,
414
+ });
415
+ continue;
416
+ }
417
+ claimedSlots[slot] = `${kind}.${twKey}`;
418
+ mappings[slot] = {
419
+ value: `${match.value}rem`,
420
+ source: `${kind}.${twKey}`,
421
+ twRem: rem,
422
+ ciaRem: match.value,
423
+ deltaRem: match.deltaRem,
424
+ confidence: match.confidence,
425
+ rationale: match.confidence === 'HIGH'
426
+ ? `Exact rem match (${rem}rem = cia ${slot}).`
427
+ : `Closest cia ${kind} slot is ${slot} (${match.value}rem); Tailwind value ${rem}rem differs by ${match.deltaRem.toFixed(4)}rem.`,
428
+ };
429
+ }
430
+
431
+ return { mappings, unmapped };
432
+ }
433
+
434
+ // ─── border radius mapping ───────────────────────────────────────────────────
435
+
436
+ function mapBorderRadius(twRadii) {
437
+ const mappings = {};
438
+ const unmapped = [];
439
+ if (!twRadii || typeof twRadii !== 'object') return { mappings, unmapped };
440
+
441
+ for (const [twKey, twVal] of Object.entries(twRadii)) {
442
+ // Tailwind uses 'full' for 9999px-ish; cia uses 'full' too
443
+ if (typeof twVal === 'string' && (twVal.includes('9999') || twVal === '9999px')) {
444
+ mappings['radius-full'] = {
445
+ value: '9999px',
446
+ source: `borderRadius.${twKey}`,
447
+ confidence: 'HIGH',
448
+ rationale: 'Tailwind \'full\' (or 9999px) maps directly to cia radius-full.',
449
+ };
450
+ continue;
451
+ }
452
+ if (twVal === '0' || twVal === 0 || twVal === '0px' || twVal === '0rem') {
453
+ mappings['radius-none'] = {
454
+ value: '0',
455
+ source: `borderRadius.${twKey}`,
456
+ confidence: 'HIGH',
457
+ };
458
+ continue;
459
+ }
460
+ const rem = parseRem(twVal);
461
+ if (rem == null) {
462
+ unmapped.push({ source: `borderRadius.${twKey}`, value: twVal, reason: 'Could not parse as a length.' });
463
+ continue;
464
+ }
465
+ const match = closestByRem(rem, CIA_RADII);
466
+ if (!match || match.key === 'full' || match.key === 'none') {
467
+ unmapped.push({ source: `borderRadius.${twKey}`, value: twVal, reason: `No cia radius within ±0.25rem of ${rem}rem.` });
468
+ continue;
469
+ }
470
+ const slot = `radius-${match.key}`;
471
+ mappings[slot] = {
472
+ value: `${match.value}rem`,
473
+ source: `borderRadius.${twKey}`,
474
+ twRem: rem,
475
+ ciaRem: match.value,
476
+ deltaRem: match.deltaRem,
477
+ confidence: match.confidence,
478
+ };
479
+ }
480
+ return { mappings, unmapped };
481
+ }
482
+
483
+ // ─── font-family mapping (always HIGH — pass-through to cia --font-* tokens) ──
484
+
485
+ function mapFontFamily(twFamilies) {
486
+ const mappings = {};
487
+ if (!twFamilies || typeof twFamilies !== 'object') return { mappings, unmapped: [] };
488
+ for (const [twKey, twVal] of Object.entries(twFamilies)) {
489
+ // cia uses font-sans, font-serif, font-mono, font-display, font-script
490
+ const ciaKey =
491
+ twKey === 'sans' ? 'font-sans' :
492
+ twKey === 'serif' ? 'font-serif' :
493
+ twKey === 'mono' ? 'font-mono' :
494
+ twKey === 'display' ? 'font-display' :
495
+ null;
496
+ if (!ciaKey) continue;
497
+ const stack = Array.isArray(twVal) ? twVal.join(', ') : String(twVal);
498
+ mappings[ciaKey] = {
499
+ value: stack,
500
+ source: `fontFamily.${twKey}`,
501
+ confidence: 'HIGH',
502
+ rationale: `Tailwind fontFamily.${twKey} → cia ${ciaKey} (direct stack copy).`,
503
+ };
504
+ }
505
+ return { mappings, unmapped: [] };
506
+ }
507
+
508
+ // ─── master mapping function ─────────────────────────────────────────────────
509
+
510
+ function mapToCia(theme) {
511
+ const allMappings = {};
512
+ const allUnmapped = [];
513
+ const counts = { HIGH: 0, MEDIUM: 0, LOW: 0, UNMAPPED: 0 };
514
+
515
+ const colors = mapColors(theme.colors);
516
+ Object.assign(allMappings, colors.mappings);
517
+ allUnmapped.push(...colors.unmapped);
518
+
519
+ const spacing = mapFlatScale(theme.spacing, CIA_SPACING, 'space', 'spacing');
520
+ Object.assign(allMappings, spacing.mappings);
521
+ allUnmapped.push(...spacing.unmapped);
522
+
523
+ const fontSize = mapFlatScale(theme.fontSize, CIA_FONT_SIZES, 'font-size', 'fontSize');
524
+ Object.assign(allMappings, fontSize.mappings);
525
+ allUnmapped.push(...fontSize.unmapped);
526
+
527
+ const radii = mapBorderRadius(theme.borderRadius);
528
+ Object.assign(allMappings, radii.mappings);
529
+ allUnmapped.push(...radii.unmapped);
530
+
531
+ const families = mapFontFamily(theme.fontFamily);
532
+ Object.assign(allMappings, families.mappings);
533
+
534
+ for (const m of Object.values(allMappings)) {
535
+ if (counts[m.confidence] != null) counts[m.confidence] += 1;
536
+ }
537
+ counts.UNMAPPED = allUnmapped.length;
538
+ counts.total = counts.HIGH + counts.MEDIUM + counts.LOW + counts.UNMAPPED;
539
+
540
+ return { mappings: allMappings, unmapped: allUnmapped, report: counts };
541
+ }
542
+
543
+ // ─── SCSS theme writer (US-03.1.4) ───────────────────────────────────────────
544
+
545
+ // Group cia tokens into sections for readable output.
546
+ function groupMappingsByCategory(mappings) {
547
+ const order = ['Colors', 'Spacing', 'Font sizes', 'Font families', 'Radii', 'Other'];
548
+ const groups = Object.fromEntries(order.map((k) => [k, []]));
549
+ for (const [token, mapping] of Object.entries(mappings)) {
550
+ if (
551
+ token.startsWith('action-') ||
552
+ token.startsWith('error-') ||
553
+ token.startsWith('success-') ||
554
+ token.startsWith('warning-') ||
555
+ token.startsWith('info-')
556
+ ) {
557
+ groups.Colors.push([token, mapping]);
558
+ } else if (token.startsWith('space-')) {
559
+ groups.Spacing.push([token, mapping]);
560
+ } else if (token.startsWith('font-size-')) {
561
+ groups['Font sizes'].push([token, mapping]);
562
+ } else if (token.startsWith('font-')) {
563
+ groups['Font families'].push([token, mapping]);
564
+ } else if (token.startsWith('radius-')) {
565
+ groups.Radii.push([token, mapping]);
566
+ } else {
567
+ groups.Other.push([token, mapping]);
568
+ }
569
+ }
570
+ return groups;
571
+ }
572
+
573
+ function confidenceTag(c) {
574
+ return c === 'HIGH' ? '' : ` /* ${c} */`;
575
+ }
576
+
577
+ // Build a cia-conformant theme.scss from a mapping. Color tokens that are
578
+ // solid hex values get derived hover/active via @include m.states(<group>).
579
+ // Unmapped Tailwind values appear as a comment block at the bottom so the
580
+ // consumer can add them as cia brand-* tokens manually.
581
+ function writeThemeScss(cia, options = {}) {
582
+ const {
583
+ name = 'migrated',
584
+ source = null,
585
+ tool = 'tailwind', // 'tailwind' | 'bootstrap' — labels the generator in the file header
586
+ timestamp = new Date().toISOString().slice(0, 19) + 'Z',
587
+ } = options;
588
+
589
+ const sourceLabel = tool === 'bootstrap' ? 'Bootstrap values' : 'Tailwind values';
590
+ const lines = [];
591
+
592
+ // ── Header (block comment so consumer sees it before any SCSS) ──
593
+ lines.push(`// ${name} theme — generated by \`cia migrate ${tool}\``);
594
+ if (source) lines.push(`// Source: ${source}`);
595
+ lines.push(`// Generated: ${timestamp}`);
596
+ lines.push(
597
+ `// Mapping confidence — HIGH: ${cia.report.HIGH} · MEDIUM: ${cia.report.MEDIUM} · LOW: ${cia.report.LOW} · UNMAPPED: ${cia.report.UNMAPPED}`,
598
+ );
599
+ lines.push(`//`);
600
+ lines.push(`// Review LOW + MEDIUM entries below — they're best-guess matches.`);
601
+ lines.push(
602
+ `// The UNMAPPED block at the bottom shows ${sourceLabel} that didn't`,
603
+ );
604
+ lines.push(`// map cleanly; add them as cia brand-* tokens or override inline.`);
605
+ lines.push(`//`);
606
+ lines.push(
607
+ `// To use: place at scss/themes/${name}.scss in your project (or any`,
608
+ );
609
+ lines.push(
610
+ `// consumer-side SCSS entry), then \`npm run build:css:themes\` if you're`,
611
+ );
612
+ lines.push(
613
+ `// inside cia's source. For a npm-consumer project, @use this file from`,
614
+ );
615
+ lines.push(`// your app's SCSS entry and set <html data-theme="${name}">.`);
616
+ lines.push(``);
617
+ lines.push(`@use 'css-is-awesome/scss/mixins' as m;`);
618
+ lines.push(``);
619
+ lines.push(`@include m.theme('${name}') {`);
620
+
621
+ // ── Token sections ──
622
+ const groups = groupMappingsByCategory(cia.mappings);
623
+ for (const [section, entries] of Object.entries(groups)) {
624
+ if (entries.length === 0) continue;
625
+ lines.push(``);
626
+ lines.push(` /* ─── ${section} ─── */`);
627
+ for (const [token, mapping] of entries) {
628
+ const tag = confidenceTag(mapping.confidence);
629
+ lines.push(` --${token}: ${mapping.value};${tag}`);
630
+ }
631
+ }
632
+
633
+ // ── Derived states for action-primary + status groups ──
634
+ const derivableGroups = ['action-primary', 'error', 'success', 'warning', 'info'];
635
+ const derived = derivableGroups.filter((g) => cia.mappings[`${g}-default`]);
636
+ if (derived.length > 0) {
637
+ lines.push(``);
638
+ lines.push(` /* ─── Derived hover + active states ─── */`);
639
+ lines.push(
640
+ ` /* m.states() emits --<group>-hover + --<group>-active via color-mix(in oklch) */`,
641
+ );
642
+ for (const g of derived) {
643
+ lines.push(` @include m.states(${g});`);
644
+ }
645
+ }
646
+
647
+ lines.push(`}`);
648
+
649
+ // ── Unmapped section as a /* */ comment block ──
650
+ if (cia.unmapped.length > 0) {
651
+ lines.push(``);
652
+ lines.push(`/* UNMAPPED — ${sourceLabel} with no direct cia analog. Review + add`);
653
+ lines.push(` * manually as cia brand-* tokens or extend the contract.`);
654
+ lines.push(` *`);
655
+ for (const u of cia.unmapped) {
656
+ const shade = u.shade ? `.${u.shade}` : '';
657
+ lines.push(` * ${u.source}${shade} = ${u.value}`);
658
+ if (u.reason) lines.push(` * ${u.reason}`);
659
+ }
660
+ lines.push(` */`);
661
+ }
662
+
663
+ return lines.join('\n') + '\n';
664
+ }
665
+
666
+ // ─── tiny flag parser ────────────────────────────────────────────────────────
667
+
668
+ function parseFlags(args) {
669
+ const flags = { name: 'migrated', out: null, json: false, help: false };
670
+ const positional = [];
671
+ for (let i = 0; i < args.length; i++) {
672
+ const a = args[i];
673
+ if (a === '--help' || a === '-h') flags.help = true;
674
+ else if (a === '--json') flags.json = true;
675
+ else if (a === '--name') flags.name = args[++i];
676
+ else if (a === '--out') flags.out = args[++i];
677
+ else if (a.startsWith('--name=')) flags.name = a.slice('--name='.length);
678
+ else if (a.startsWith('--out=')) flags.out = a.slice('--out='.length);
679
+ else positional.push(a);
680
+ }
681
+ return { flags, positional };
682
+ }
683
+
684
+ const HELP = `cia migrate tailwind — convert tailwind.config.* to a cia theme.scss
685
+
686
+ Usage:
687
+ cia migrate tailwind [path] [options]
688
+
689
+ Default behavior:
690
+ Parses the config, maps Tailwind tokens to cia contract tokens (HIGH/
691
+ MEDIUM/LOW/UNMAPPED confidence), and writes a cia theme.scss to
692
+ ./cia-themes/<name>.scss. Diagnostics print to stderr.
693
+
694
+ Arguments:
695
+ path Optional. Path to tailwind.config.{js,ts,mjs,cjs}. If
696
+ omitted, searches the current directory upward.
697
+
698
+ Options:
699
+ --name <name> Theme name to use in @include m.theme('<name>') and as
700
+ the default filename. Default: migrated
701
+ --out <path> Write to this path instead of the default location.
702
+ --json Skip the file write and dump the full JSON (theme + cia
703
+ mappings + unmapped + report) to stdout. Useful for
704
+ scripts. Diagnostics still print to stderr.
705
+ -h, --help Show this help.
706
+
707
+ Examples:
708
+ cia migrate tailwind ./tailwind.config.js
709
+ cia migrate tailwind --name acme
710
+ cia migrate tailwind --out ./scss/themes/brand.scss --name brand
711
+ cia migrate tailwind --json > theme.json # pipe-safe JSON
712
+ cia migrate tailwind --json | jq '.cia.report'
713
+ `;
714
+
715
+ async function run(args) {
716
+ const { flags, positional } = parseFlags(args);
717
+ if (flags.help) {
718
+ process.stdout.write(HELP);
719
+ return;
720
+ }
721
+
722
+ let configPath = positional[0];
723
+ if (configPath) {
724
+ if (!path.isAbsolute(configPath)) {
725
+ configPath = path.resolve(process.cwd(), configPath);
726
+ }
727
+ if (!fs.existsSync(configPath)) {
728
+ throw new Error(`config path not found: ${configPath}`);
729
+ }
730
+ } else {
731
+ configPath = findTailwindConfig();
732
+ if (!configPath) {
733
+ throw new Error(
734
+ `no tailwind.config.{js,ts,mjs,cjs} found in current directory or any parent. ` +
735
+ `Pass an explicit path: cia migrate tailwind <path>`,
736
+ );
737
+ }
738
+ }
739
+
740
+ process.stderr.write(`cia migrate tailwind\n`);
741
+ process.stderr.write(` config: ${configPath}\n`);
742
+ process.stderr.write(` loading...\n`);
743
+
744
+ const rawConfig = loadConfig(configPath);
745
+ if (!rawConfig || typeof rawConfig !== 'object') {
746
+ throw new Error(`config did not export a valid object`);
747
+ }
748
+
749
+ process.stderr.write(` resolving (tailwindcss/resolveConfig if available)...\n`);
750
+ const { resolved, resolverUsed } = resolveTailwindConfig(rawConfig);
751
+ process.stderr.write(` resolver: ${resolverUsed}\n`);
752
+ const theme = extractTheme(resolved);
753
+ const stats = summary(theme);
754
+
755
+ process.stderr.write(` extracted theme fields:\n`);
756
+ for (const [field, count] of Object.entries(stats)) {
757
+ process.stderr.write(` ${field.padEnd(14)}: ${count} values\n`);
758
+ }
759
+
760
+ process.stderr.write(`\n mapping to cia contract tokens...\n`);
761
+ const cia = mapToCia(theme);
762
+
763
+ process.stderr.write(`\n ─── confidence report ─────────────────────────\n`);
764
+ process.stderr.write(` HIGH (exact match): ${cia.report.HIGH}\n`);
765
+ process.stderr.write(` MEDIUM (close, ≤0.125rem): ${cia.report.MEDIUM}\n`);
766
+ process.stderr.write(` LOW (best guess): ${cia.report.LOW}\n`);
767
+ process.stderr.write(` UNMAPPED (no cia analog): ${cia.report.UNMAPPED}\n`);
768
+ process.stderr.write(` ─────────────────────────────────────────────\n`);
769
+ process.stderr.write(` total ${cia.report.total}\n`);
770
+
771
+ // ── Output: either the SCSS file (default) or JSON (--json) ──
772
+ if (flags.json) {
773
+ if (cia.report.UNMAPPED > 0) {
774
+ process.stderr.write(`\n ${cia.report.UNMAPPED} unmapped value(s) — see .cia.unmapped in the JSON below\n`);
775
+ }
776
+ process.stderr.write(`\n --- JSON dump on stdout below ---\n`);
777
+ const output = {
778
+ source: configPath,
779
+ cia_cli_version_phase: 'PR 3 — parse + map + write (JSON mode via --json)',
780
+ summary: stats,
781
+ theme,
782
+ cia,
783
+ };
784
+ process.stdout.write(JSON.stringify(output, null, 2));
785
+ process.stdout.write('\n');
786
+ return;
787
+ }
788
+
789
+ // Write the theme.scss file
790
+ const scss = writeThemeScss(cia, { name: flags.name, source: configPath });
791
+ let outPath = flags.out;
792
+ if (outPath) {
793
+ if (!path.isAbsolute(outPath)) outPath = path.resolve(process.cwd(), outPath);
794
+ } else {
795
+ outPath = path.join(process.cwd(), 'cia-themes', `${flags.name}.scss`);
796
+ }
797
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
798
+ fs.writeFileSync(outPath, scss, 'utf8');
799
+
800
+ process.stderr.write(`\n wrote ${outPath}\n`);
801
+ process.stderr.write(` ${scss.split('\n').length - 1} lines, ${Object.keys(cia.mappings).length} mapped tokens\n`);
802
+ if (cia.report.UNMAPPED > 0) {
803
+ process.stderr.write(` ${cia.report.UNMAPPED} unmapped value(s) appended as /* */ comment block — review + add manually\n`);
804
+ }
805
+ process.stderr.write(`\n Next steps:\n`);
806
+ process.stderr.write(` 1. Review the file (LOW/MEDIUM/UNMAPPED entries are flagged)\n`);
807
+ process.stderr.write(` 2. @use this file from your app's SCSS entry\n`);
808
+ process.stderr.write(` 3. Set <html data-theme="${flags.name}"> and run npm run build:css\n`);
809
+ process.stderr.write(` 4. Run npm run validate-themes to confirm WCAG 2.2 AA contrast\n`);
810
+ }
811
+
812
+ module.exports = {
813
+ run,
814
+ findTailwindConfig,
815
+ extractTheme,
816
+ summary,
817
+ // Mapping internals exported for unit testing + external introspection
818
+ mapToCia,
819
+ mapColors,
820
+ mapFlatScale,
821
+ mapBorderRadius,
822
+ mapFontFamily,
823
+ closestByRem,
824
+ parseRem,
825
+ CIA_SPACING,
826
+ CIA_FONT_SIZES,
827
+ CIA_RADII,
828
+ // Writer + flag parser
829
+ writeThemeScss,
830
+ groupMappingsByCategory,
831
+ parseFlags,
832
+ };