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.
- package/AGENTS.md +343 -0
- package/CHANGELOG.md +1036 -0
- package/CLAUDE.md +9 -0
- package/CONTRACT.md +580 -0
- package/GEMINI.md +9 -0
- package/LICENSE +21 -0
- package/LICENSE-third-party +88 -0
- package/MIGRATION.md +312 -0
- package/README.md +380 -0
- package/ROADMAP.md +714 -0
- package/THEMING.md +290 -0
- package/THREE-TIERS.md +158 -0
- package/VERSIONING.md +200 -0
- package/_index.scss +34 -0
- package/api.scss +16 -0
- package/bin/README.md +57 -0
- package/bin/cia.cjs +114 -0
- package/bin/migrate-bootstrap.cjs +427 -0
- package/bin/migrate-tailwind.cjs +832 -0
- package/css-is-awesome.instructions.md +533 -0
- package/dist/css-is-awesome.core.css +598 -0
- package/dist/css-is-awesome.core.min.css +1 -0
- package/dist/css-is-awesome.css +3733 -0
- package/dist/css-is-awesome.min.css +1 -0
- package/dist/css-is-awesome.utilities.css +2445 -0
- package/dist/css-is-awesome.utilities.min.css +1 -0
- package/dist/tokens.css +508 -0
- package/dist/tokens.d.ts +266 -0
- package/dist/tokens.min.css +1 -0
- package/figma-tokens/README.md +57 -0
- package/figma-tokens/primitives-brand.scss +121 -0
- package/figma-tokens/semantic-dark.scss +61 -0
- package/figma-tokens/semantic-light.scss +61 -0
- package/figma-tokens/tokens.json +144 -0
- package/llm.txt +183 -0
- package/mcp/server.cjs +1495 -0
- package/package.json +199 -0
- package/public/icons/README.md +278 -0
- package/public/icons/arrow-right.svg +4 -0
- package/public/icons/check.svg +4 -0
- package/public/icons/chevron-down.svg +4 -0
- package/public/icons/close.svg +4 -0
- package/public/icons/core/arrow-down.svg +13 -0
- package/public/icons/core/arrow-left.svg +13 -0
- package/public/icons/core/arrow-right.svg +13 -0
- package/public/icons/core/arrow-up.svg +13 -0
- package/public/icons/core/bell.svg +13 -0
- package/public/icons/core/calendar.svg +15 -0
- package/public/icons/core/check.svg +12 -0
- package/public/icons/core/chevron-down.svg +12 -0
- package/public/icons/core/chevron-left.svg +12 -0
- package/public/icons/core/chevron-right.svg +12 -0
- package/public/icons/core/chevron-up.svg +12 -0
- package/public/icons/core/clock.svg +13 -0
- package/public/icons/core/close.svg +13 -0
- package/public/icons/core/copy.svg +13 -0
- package/public/icons/core/download.svg +14 -0
- package/public/icons/core/edit.svg +13 -0
- package/public/icons/core/error.svg +14 -0
- package/public/icons/core/external-link.svg +14 -0
- package/public/icons/core/eye-off.svg +15 -0
- package/public/icons/core/eye.svg +13 -0
- package/public/icons/core/filter.svg +12 -0
- package/public/icons/core/heart.svg +12 -0
- package/public/icons/core/help.svg +14 -0
- package/public/icons/core/home.svg +13 -0
- package/public/icons/core/info.svg +14 -0
- package/public/icons/core/loading.svg +12 -0
- package/public/icons/core/lock.svg +13 -0
- package/public/icons/core/mail.svg +13 -0
- package/public/icons/core/menu.svg +14 -0
- package/public/icons/core/message.svg +12 -0
- package/public/icons/core/minus.svg +12 -0
- package/public/icons/core/more-horizontal.svg +14 -0
- package/public/icons/core/pause.svg +13 -0
- package/public/icons/core/play.svg +12 -0
- package/public/icons/core/plus.svg +13 -0
- package/public/icons/core/refresh.svg +15 -0
- package/public/icons/core/save.svg +14 -0
- package/public/icons/core/search.svg +13 -0
- package/public/icons/core/settings.svg +13 -0
- package/public/icons/core/share.svg +14 -0
- package/public/icons/core/sort.svg +15 -0
- package/public/icons/core/star.svg +12 -0
- package/public/icons/core/success.svg +13 -0
- package/public/icons/core/trash.svg +16 -0
- package/public/icons/core/unlock.svg +13 -0
- package/public/icons/core/upload.svg +14 -0
- package/public/icons/core/user.svg +13 -0
- package/public/icons/core/users.svg +15 -0
- package/public/icons/core/warning.svg +14 -0
- package/public/icons/download.svg +4 -0
- package/public/icons/edit.svg +4 -0
- package/public/icons/menu.svg +4 -0
- package/public/icons/search.svg +4 -0
- package/public/theme.css +4179 -0
- package/public/themes/README.md +102 -0
- package/public/themes/boilerplate/theme.css +154 -0
- package/public/themes/boilerplate-dark/theme.css +171 -0
- package/public/themes/boilerplate-light/theme.css +171 -0
- package/public/themes/cupertino/theme.css +165 -0
- package/public/themes/cupertino-dark/theme.css +200 -0
- package/public/themes/cupertino-light/theme.css +227 -0
- package/public/themes/glass/theme.css +190 -0
- package/public/themes/glass-dark/theme.css +201 -0
- package/public/themes/glass-light/theme.css +229 -0
- package/public/themes/graphite/theme.css +155 -0
- package/public/themes/graphite-dark/theme.css +223 -0
- package/public/themes/graphite-light/theme.css +191 -0
- package/public/themes/press/theme.css +189 -0
- package/public/themes/press-dark/theme.css +199 -0
- package/public/themes/press-light/theme.css +235 -0
- package/public/themes/prism/theme.css +159 -0
- package/public/themes/prism-dark/theme.css +185 -0
- package/public/themes/prism-light/theme.css +188 -0
- package/public/themes/sketchbook/theme.css +152 -0
- package/public/themes/sketchbook-dark/theme.css +194 -0
- package/public/themes/sketchbook-light/theme.css +188 -0
- package/public/themes/terminal/theme.css +159 -0
- package/public/themes/terminal-dark/theme.css +229 -0
- package/public/themes/terminal-light/theme.css +152 -0
- package/scripts/README.md +70 -0
- package/scripts/icon-contract.json +67 -0
- package/scripts/icon-validator.js +205 -0
- package/scripts/prepare-dist.mjs +65 -0
- package/scripts/theme-a11y.js +415 -0
- package/scripts/theme-contract.json +171 -0
- package/scripts/theme-validator.js +674 -0
- package/scss/README.md +55 -0
- package/scss/_animations-utilities.scss +107 -0
- package/scss/_animations.scss +248 -0
- package/scss/_app-styles.scss +15 -0
- package/scss/_generator.scss +174 -0
- package/scss/_icons.scss +475 -0
- package/scss/_index.scss +17 -0
- package/scss/_layout.scss +597 -0
- package/scss/_mixins.scss +1036 -0
- package/scss/_system.scss +291 -0
- package/scss/_utilities.scss +377 -0
- package/scss/api.scss +61 -0
- package/scss/components/_accordion.scss +72 -0
- package/scss/components/_buttons.scss +171 -0
- package/scss/components/_copy-button.scss +69 -0
- package/scss/components/_data.scss +190 -0
- package/scss/components/_feedback.scss +197 -0
- package/scss/components/_forms.scss +272 -0
- package/scss/components/_index.scss +17 -0
- package/scss/components/_navigation.scss +186 -0
- package/scss/components/_overlay.scss +259 -0
- package/scss/components/_stepper.scss +159 -0
- package/scss/components/_tabs.scss +72 -0
- package/scss/core.scss +54 -0
- package/scss/examples/_theming.scss +107 -0
- package/scss/examples/_usage.scss +317 -0
- package/scss/main.scss +64 -0
- package/scss/recipes/README.md +110 -0
- package/scss/recipes/_bare-tags.scss +232 -0
- package/scss/recipes/_recipe-template.md +119 -0
- package/scss/recipes/combobox.md +509 -0
- package/scss/recipes/dialog.md +265 -0
- package/scss/recipes/print-to-pdf.md +263 -0
- package/scss/theme/README.md +67 -0
- package/scss/theme/_brand.scss +18 -0
- package/scss/theme/_colors-dark.scss +68 -0
- package/scss/theme/_colors-light.scss +70 -0
- package/scss/theme/_components.scss +94 -0
- package/scss/theme/_icons.scss +104 -0
- package/scss/theme/_index.scss +62 -0
- package/scss/theme/_shadows.scss +65 -0
- package/scss/theme/_shape.scss +13 -0
- package/scss/theme/_typography.scss +9 -0
- package/scss/theme/registry.json +447 -0
- package/scss/themes/boilerplate-dark.scss +212 -0
- package/scss/themes/boilerplate-light.scss +209 -0
- package/scss/themes/boilerplate.scss +181 -0
- package/scss/themes/cupertino-dark.scss +238 -0
- package/scss/themes/cupertino-light.scss +266 -0
- package/scss/themes/cupertino.scss +194 -0
- package/scss/themes/glass-dark.scss +238 -0
- package/scss/themes/glass-light.scss +266 -0
- package/scss/themes/glass.scss +220 -0
- package/scss/themes/graphite-dark.scss +260 -0
- package/scss/themes/graphite-light.scss +228 -0
- package/scss/themes/graphite.scss +182 -0
- package/scss/themes/press-dark.scss +237 -0
- package/scss/themes/press-light.scss +273 -0
- package/scss/themes/press.scss +241 -0
- package/scss/themes/prism-dark.scss +217 -0
- package/scss/themes/prism-light.scss +220 -0
- package/scss/themes/prism.scss +190 -0
- package/scss/themes/sketchbook-dark.scss +231 -0
- package/scss/themes/sketchbook-light.scss +231 -0
- package/scss/themes/sketchbook.scss +178 -0
- package/scss/themes/terminal-dark.scss +267 -0
- package/scss/themes/terminal-light.scss +175 -0
- package/scss/themes/terminal.scss +185 -0
- package/scss/tokens.scss +23 -0
- package/scss/utilities-only.scss +4 -0
package/api.scss
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Package-root shim for `@use 'css-is-awesome/api'`
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Sass does NOT read package.json "exports". It resolves a bare specifier by
|
|
5
|
+
// looking for a file on disk relative to a load path — for
|
|
6
|
+
// `css-is-awesome/api` that means `node_modules/css-is-awesome/api.scss`.
|
|
7
|
+
// The real barrel lives at `scss/api.scss`, so without this shim the import
|
|
8
|
+
// documented in the README and AGENTS.md fails on a clean install with
|
|
9
|
+
// "Can't find stylesheet to import."
|
|
10
|
+
//
|
|
11
|
+
// This file exists purely so the documented specifier resolves. It forwards
|
|
12
|
+
// and emits nothing, so `css-is-awesome/api` stays zero-emit.
|
|
13
|
+
//
|
|
14
|
+
// Keep in sync with the "./api" entry in package.json "exports" — that entry
|
|
15
|
+
// serves bundlers that DO honour exports; this file serves Sass itself.
|
|
16
|
+
@forward './scss/api';
|
package/bin/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# bin/
|
|
2
|
+
|
|
3
|
+
cia's CLI entry points. **These are the only JavaScript files cia ships** — the package itself remains JS-free per the locked architecture rule (`feedback_no_js_in_package.md`); the CLI tools in `bin/` are explicitly allowed because they're consumer-facing tooling, not consumed by browser code at runtime.
|
|
4
|
+
|
|
5
|
+
## Bins registered in `package.json`
|
|
6
|
+
|
|
7
|
+
| Bin | File | What it does |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `css-is-awesome-mcp` | `../mcp/server.cjs` | MCP stdio server for AI agents (themes, mixins, recipes, etc.) |
|
|
10
|
+
| `cia` | `./cia.cjs` | The CLI — currently supports `migrate tailwind`; more subcommands planned |
|
|
11
|
+
|
|
12
|
+
Invoke either via `npx` from a consumer project that has cia installed:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx css-is-awesome-mcp # MCP server (stdio; configure via .mcp.json)
|
|
16
|
+
npx cia --help # CLI help
|
|
17
|
+
npx cia migrate tailwind ./tailwind.config.js
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Subcommand files
|
|
21
|
+
|
|
22
|
+
`cia.cjs` is the router. Each subcommand lives in its own sibling file and is loaded lazily so consumers who only use one path don't pay the require cost of the others.
|
|
23
|
+
|
|
24
|
+
| Subcommand | File | Status |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `cia migrate tailwind <path>` | `migrate-tailwind.cjs` | **PR 1**: parses + dumps theme JSON. Future PRs: map tokens + emit theme.scss. |
|
|
27
|
+
| `cia migrate bootstrap <path>` | (planned) | Roadmap: `roadmap/epics/v1-0/EPIC-03-migration-on-ramp.md` F3.2 |
|
|
28
|
+
|
|
29
|
+
## Adding a new subcommand
|
|
30
|
+
|
|
31
|
+
1. Create `bin/<command>.cjs` that exports `module.exports = { run: async (args) => {...} }`
|
|
32
|
+
2. Add a route in `bin/cia.cjs`:
|
|
33
|
+
```js
|
|
34
|
+
if (command === 'newcommand') {
|
|
35
|
+
const { run } = require('./newcommand.cjs');
|
|
36
|
+
await run(rest);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
3. Add help text in `bin/cia.cjs`'s `HELP` constant
|
|
41
|
+
4. Document in this README + add an entry to the relevant epic file in `roadmap/epics/`
|
|
42
|
+
|
|
43
|
+
## Optional peer dependencies
|
|
44
|
+
|
|
45
|
+
The CLI gracefully degrades when consumer-project deps aren't installed:
|
|
46
|
+
|
|
47
|
+
- **`jiti`** — required to load `tailwind.config.ts` / `.mjs`. `.js` / `.cjs` work without it. If a consumer hits a `.ts` config without jiti, the CLI prints a clear install hint.
|
|
48
|
+
- **`tailwindcss`** — when present, the CLI uses Tailwind's own `resolveConfig` so `extends`, plugins, and defaults are folded in. When absent, the raw config is parsed directly (less complete, but enough for inspection).
|
|
49
|
+
|
|
50
|
+
cia doesn't depend on either at runtime — both are loaded inside `try/catch`. cia ships with neither in its own `dependencies` field; they're optional peer deps for the consumer.
|
|
51
|
+
|
|
52
|
+
## Architecture rules these binaries respect
|
|
53
|
+
|
|
54
|
+
- Zero JS in npm `files` manifest beyond `bin/` and `mcp/` (and `dist/` for compiled CSS)
|
|
55
|
+
- No browser code in these bins — they run in Node, in the consumer's terminal
|
|
56
|
+
- No telemetry, no network calls (other than what `tailwindcss/resolveConfig` does internally, which is none)
|
|
57
|
+
- Errors print to stderr; structured output prints to stdout (pipe-safe)
|
package/bin/cia.cjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cia — css-is-awesome CLI
|
|
4
|
+
*
|
|
5
|
+
* Subcommand router. Each subcommand lives in its own file under bin/ and
|
|
6
|
+
* exposes a `run(args)` async function.
|
|
7
|
+
*
|
|
8
|
+
* Status: both converters shipped (EPIC-03 migration on-ramp, 6/6).
|
|
9
|
+
* migrate tailwind — parse tailwind.config.* + dump theme JSON
|
|
10
|
+
* migrate bootstrap — parse Bootstrap SCSS/CSS vars + dump theme JSON
|
|
11
|
+
*
|
|
12
|
+
* cia core ships ZERO JavaScript in the `files` manifest. The CLI lives in
|
|
13
|
+
* `bin/` which is explicitly allowed per the architecture lock — same path
|
|
14
|
+
* as the MCP server (mcp/server.cjs). cia core remains a styling
|
|
15
|
+
* foundation; this CLI is consumer-facing tooling, not consumed by browser
|
|
16
|
+
* code at runtime.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const [command, ...rest] = args;
|
|
22
|
+
|
|
23
|
+
const HELP = `cia — css-is-awesome CLI
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
cia <command> [options]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
migrate <tool> [path] Convert another design system's config to a cia
|
|
30
|
+
theme. Tools: tailwind | bootstrap (planned).
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
cia migrate tailwind ./tailwind.config.js
|
|
34
|
+
cia migrate tailwind # auto-detect tailwind.config.*
|
|
35
|
+
|
|
36
|
+
Run \`cia <command> --help\` for command-specific help.
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const MIGRATE_HELP = `cia migrate — convert another design system's config to a cia theme
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
cia migrate <tool> [path] [options]
|
|
43
|
+
|
|
44
|
+
Tools:
|
|
45
|
+
tailwind Read a tailwind.config.{js,ts,mjs,cjs} and write a cia
|
|
46
|
+
theme.scss with HIGH/MEDIUM/LOW/UNMAPPED confidence levels.
|
|
47
|
+
Run \`cia migrate tailwind --help\` for full options.
|
|
48
|
+
|
|
49
|
+
bootstrap Read a Bootstrap _variables.scss (or custom-variables.scss)
|
|
50
|
+
and write a cia theme.scss. Maps \$primary / \$body-bg /
|
|
51
|
+
\$border-radius / \$font-family-base / \$spacer / status colors
|
|
52
|
+
via Bootstrap convention.
|
|
53
|
+
Run \`cia migrate bootstrap --help\` for full options.
|
|
54
|
+
|
|
55
|
+
Common options (both tools):
|
|
56
|
+
--name <name> Theme name. Default: migrated
|
|
57
|
+
--out <path> Custom output path. Default: ./cia-themes/<name>.scss
|
|
58
|
+
--json Skip the file write and dump JSON to stdout (pipe-safe).
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
cia migrate tailwind ./tailwind.config.js
|
|
62
|
+
cia migrate bootstrap ./scss/_variables.scss --name acme
|
|
63
|
+
cia migrate bootstrap ./scss/_variables.scss --json | jq '.cia.report'
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
function fail(message, exit = 1) {
|
|
67
|
+
process.stderr.write(`cia: ${message}\n`);
|
|
68
|
+
process.exit(exit);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
if (!command || command === '-h' || command === '--help' || command === 'help') {
|
|
73
|
+
process.stdout.write(HELP);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (command === '--version' || command === '-v') {
|
|
78
|
+
// Read version from sibling package.json
|
|
79
|
+
const path = require('path');
|
|
80
|
+
const fs = require('fs');
|
|
81
|
+
try {
|
|
82
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
83
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
84
|
+
return;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
fail(`could not read version: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (command === 'migrate') {
|
|
91
|
+
const [tool, ...migrateArgs] = rest;
|
|
92
|
+
if (!tool || tool === '-h' || tool === '--help' || tool === 'help') {
|
|
93
|
+
process.stdout.write(MIGRATE_HELP);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (tool === 'tailwind') {
|
|
97
|
+
const { run } = require('./migrate-tailwind.cjs');
|
|
98
|
+
await run(migrateArgs);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (tool === 'bootstrap') {
|
|
102
|
+
const { run } = require('./migrate-bootstrap.cjs');
|
|
103
|
+
await run(migrateArgs);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
fail(`unknown migrate tool '${tool}'. Available: tailwind, bootstrap.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fail(`unknown command '${command}'. Run \`cia --help\` for usage.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((err) => {
|
|
113
|
+
fail(err && err.message ? err.message : String(err));
|
|
114
|
+
});
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cia migrate bootstrap — parse a Bootstrap _variables.scss + map to cia.
|
|
3
|
+
*
|
|
4
|
+
* US-03.2.1 + US-03.2.2 (PR 4 of EPIC-03). Same shape as migrate-tailwind:
|
|
5
|
+
* 1. Parse the input file (Bootstrap variables in this case)
|
|
6
|
+
* 2. Map to cia contract tokens with confidence levels
|
|
7
|
+
* 3. Write a cia theme.scss (via writeThemeScss from migrate-tailwind.cjs)
|
|
8
|
+
*
|
|
9
|
+
* cia migrate bootstrap leans on Bootstrap's well-established variable
|
|
10
|
+
* naming convention. Bootstrap's $primary maps to cia's action-primary-
|
|
11
|
+
* default; $body-bg maps to background-default; $border-radius to
|
|
12
|
+
* radius-md, etc. Some Bootstrap variables ($light, $dark utility colors;
|
|
13
|
+
* Sass map values like $spacers) don't have a clean cia analog and surface
|
|
14
|
+
* in the UNMAPPED block for manual review.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
// Shared writer + flag parser from the Tailwind module — no need to fork.
|
|
22
|
+
const {
|
|
23
|
+
writeThemeScss,
|
|
24
|
+
parseFlags,
|
|
25
|
+
parseRem,
|
|
26
|
+
closestByRem,
|
|
27
|
+
CIA_RADII,
|
|
28
|
+
CIA_FONT_SIZES,
|
|
29
|
+
} = require('./migrate-tailwind.cjs');
|
|
30
|
+
|
|
31
|
+
// ─── Bootstrap variable parser ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
// Walk a SCSS file line-by-line. Capture `$var: value [!default];` patterns,
|
|
34
|
+
// honoring parenthesis balance so multi-line map/list literals don't split.
|
|
35
|
+
// Returns an object: { name: value }, with $ prefix stripped. !default
|
|
36
|
+
// declarations capture the value; later non-default declarations overwrite.
|
|
37
|
+
function parseBootstrapVars(scssText) {
|
|
38
|
+
const vars = {};
|
|
39
|
+
const lines = scssText.split(/\r?\n/);
|
|
40
|
+
let buffer = '';
|
|
41
|
+
let parenDepth = 0;
|
|
42
|
+
|
|
43
|
+
for (const rawLine of lines) {
|
|
44
|
+
// Strip line comments (but preserve in-string // — rare in Bootstrap)
|
|
45
|
+
const line = rawLine.replace(/\/\/.*$/, '').trim();
|
|
46
|
+
if (!line && parenDepth === 0) {
|
|
47
|
+
// Blank line outside an open paren ends any unterminated statement
|
|
48
|
+
buffer = '';
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
|
|
53
|
+
buffer += (buffer ? ' ' : '') + line;
|
|
54
|
+
for (const ch of line) {
|
|
55
|
+
if (ch === '(') parenDepth++;
|
|
56
|
+
else if (ch === ')') parenDepth = Math.max(0, parenDepth - 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parenDepth === 0 && buffer.endsWith(';')) {
|
|
60
|
+
const stmt = buffer.slice(0, -1).trim();
|
|
61
|
+
// Match $name: value [!default]
|
|
62
|
+
const m = stmt.match(/^(\$[\w-]+)\s*:\s*([\s\S]+?)(\s*!default)?\s*$/);
|
|
63
|
+
if (m) {
|
|
64
|
+
const [, name, rawValue, isDefault] = m;
|
|
65
|
+
const value = rawValue.trim();
|
|
66
|
+
const key = name.slice(1);
|
|
67
|
+
// Honor Bootstrap's override convention: non-default wins over default;
|
|
68
|
+
// we record the most recent non-default, else the most recent default.
|
|
69
|
+
const existing = vars[key];
|
|
70
|
+
const incomingIsDefault = !!isDefault;
|
|
71
|
+
if (!existing || !existing.isDefault || incomingIsDefault === false) {
|
|
72
|
+
vars[key] = { value, isDefault: incomingIsDefault };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
buffer = '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Strip the {value, isDefault} envelope — just return value strings, but
|
|
80
|
+
// keep an isDefault map for diagnostics.
|
|
81
|
+
const out = {};
|
|
82
|
+
const meta = {};
|
|
83
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
84
|
+
out[k] = v.value;
|
|
85
|
+
meta[k] = { isDefault: v.isDefault };
|
|
86
|
+
}
|
|
87
|
+
return { vars: out, meta };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Bootstrap → cia mapping ─────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
// Bootstrap variable names → cia contract token slot.
|
|
93
|
+
// HIGH-confidence mappings: semantic alignment is well-established.
|
|
94
|
+
const BOOTSTRAP_DIRECT_MAP = {
|
|
95
|
+
// Status / brand colors
|
|
96
|
+
primary: 'action-primary-default',
|
|
97
|
+
secondary: 'action-secondary-default',
|
|
98
|
+
success: 'success-default',
|
|
99
|
+
info: 'info-default',
|
|
100
|
+
warning: 'warning-default',
|
|
101
|
+
danger: 'error-default', // Bootstrap "danger" → cia "error"
|
|
102
|
+
|
|
103
|
+
// Surfaces + text
|
|
104
|
+
'body-bg': 'background-default',
|
|
105
|
+
'body-color': 'text-primary',
|
|
106
|
+
'body-secondary-bg': 'background-subtle',
|
|
107
|
+
'body-tertiary-bg': 'surface-muted',
|
|
108
|
+
'body-secondary-color': 'text-secondary',
|
|
109
|
+
'body-tertiary-color': 'text-muted',
|
|
110
|
+
|
|
111
|
+
// Border
|
|
112
|
+
'border-color': 'border-default',
|
|
113
|
+
'border-color-translucent': 'border-subtle',
|
|
114
|
+
|
|
115
|
+
// Type
|
|
116
|
+
'font-family-base': 'font-sans',
|
|
117
|
+
'font-family-monospace': 'font-mono',
|
|
118
|
+
'font-family-serif': 'font-serif',
|
|
119
|
+
|
|
120
|
+
// Spacing baseline
|
|
121
|
+
spacer: 'space-4',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Bootstrap utility colors that have NO clean cia analog — list explicitly so
|
|
125
|
+
// the UNMAPPED block can carry helpful guidance.
|
|
126
|
+
const BOOTSTRAP_UTILITY_COLORS = new Set([
|
|
127
|
+
'light', 'dark', 'white', 'black', 'gray',
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Variables whose value is a Sass expression / variable reference we can't
|
|
131
|
+
// resolve without a full Sass evaluator. Surfaces as UNMAPPED with a note.
|
|
132
|
+
function isUnresolvableExpression(value) {
|
|
133
|
+
if (typeof value !== 'string') return true;
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
// Map / list literals
|
|
136
|
+
if (trimmed.startsWith('(')) return true;
|
|
137
|
+
// Sass variable reference (e.g. `$white`, `$gray-900`)
|
|
138
|
+
if (trimmed.startsWith('$')) return true;
|
|
139
|
+
// Math expression with named vars (heuristic: contains $ followed by word)
|
|
140
|
+
if (/\$[a-z]/i.test(trimmed)) return true;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Strip Sass-isms from a value where possible (var(--bs-...) wrappers etc.)
|
|
145
|
+
function cleanValue(value) {
|
|
146
|
+
if (typeof value !== 'string') return value;
|
|
147
|
+
// Bootstrap 5+ uses var(--bs-font-sans-serif) etc.; pass through as-is —
|
|
148
|
+
// these are valid CSS at runtime.
|
|
149
|
+
return value.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mapBootstrapToCia(vars, meta) {
|
|
153
|
+
const mappings = {};
|
|
154
|
+
const unmapped = [];
|
|
155
|
+
|
|
156
|
+
for (const [name, rawValue] of Object.entries(vars)) {
|
|
157
|
+
const value = cleanValue(rawValue);
|
|
158
|
+
const sourceLabel = `\$${name}${meta[name] && meta[name].isDefault ? ' !default' : ''}`;
|
|
159
|
+
|
|
160
|
+
// 1) Direct mapping table — HIGH confidence semantic match
|
|
161
|
+
if (BOOTSTRAP_DIRECT_MAP[name]) {
|
|
162
|
+
if (isUnresolvableExpression(value)) {
|
|
163
|
+
unmapped.push({
|
|
164
|
+
source: sourceLabel,
|
|
165
|
+
value,
|
|
166
|
+
reason: `'\$${name}' references another Sass variable or expression we can't evaluate without compiling. Resolve manually (e.g. paste the literal hex/rem value) then re-run.`,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
mappings[BOOTSTRAP_DIRECT_MAP[name]] = {
|
|
171
|
+
value,
|
|
172
|
+
source: sourceLabel,
|
|
173
|
+
confidence: 'HIGH',
|
|
174
|
+
rationale: `Bootstrap '\$${name}' → cia '${BOOTSTRAP_DIRECT_MAP[name]}' (semantic equivalent in established Bootstrap convention).`,
|
|
175
|
+
};
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 2) Border radius family — closest cia radius by rem proximity
|
|
180
|
+
if (/^border-radius(-[\w-]+)?$/.test(name)) {
|
|
181
|
+
if (name === 'border-radius') {
|
|
182
|
+
// Bootstrap's default border-radius → cia radius-md per convention
|
|
183
|
+
if (!isUnresolvableExpression(value)) {
|
|
184
|
+
mappings['radius-md'] = {
|
|
185
|
+
value,
|
|
186
|
+
source: sourceLabel,
|
|
187
|
+
confidence: 'HIGH',
|
|
188
|
+
rationale: `Bootstrap's '\$border-radius' (base) → cia radius-md by convention.`,
|
|
189
|
+
};
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// border-radius-sm / -lg / -xl etc. — rem proximity
|
|
194
|
+
const rem = parseRem(value);
|
|
195
|
+
if (rem != null) {
|
|
196
|
+
const match = closestByRem(rem, CIA_RADII);
|
|
197
|
+
if (match && match.key !== 'full' && match.key !== 'none') {
|
|
198
|
+
const slot = `radius-${match.key}`;
|
|
199
|
+
mappings[slot] = {
|
|
200
|
+
value: `${match.value}rem`,
|
|
201
|
+
source: sourceLabel,
|
|
202
|
+
twRem: rem,
|
|
203
|
+
ciaRem: match.value,
|
|
204
|
+
deltaRem: match.deltaRem,
|
|
205
|
+
confidence: match.confidence,
|
|
206
|
+
rationale: match.confidence === 'HIGH'
|
|
207
|
+
? `Bootstrap '\$${name}' (${rem}rem) → cia ${slot} (exact match).`
|
|
208
|
+
: `Bootstrap '\$${name}' (${rem}rem) → cia ${slot} (${match.value}rem); delta ${match.deltaRem.toFixed(4)}rem.`,
|
|
209
|
+
};
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
unmapped.push({
|
|
214
|
+
source: sourceLabel,
|
|
215
|
+
value,
|
|
216
|
+
reason: `Could not place '\$${name}' on cia's radius scale.`,
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 3) Font sizes — Bootstrap has font-size-base, font-size-sm, etc.
|
|
222
|
+
if (name === 'font-size-base') {
|
|
223
|
+
if (!isUnresolvableExpression(value)) {
|
|
224
|
+
const rem = parseRem(value);
|
|
225
|
+
if (rem != null) {
|
|
226
|
+
// 1rem is Bootstrap's default = cia font-size-3 (1rem)
|
|
227
|
+
mappings['font-size-3'] = {
|
|
228
|
+
value,
|
|
229
|
+
source: sourceLabel,
|
|
230
|
+
confidence: 'HIGH',
|
|
231
|
+
rationale: `Bootstrap '\$font-size-base' (typically 1rem) → cia font-size-3 by convention.`,
|
|
232
|
+
};
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (/^font-size-(sm|lg|xl|xxl)$/.test(name)) {
|
|
238
|
+
const rem = parseRem(value);
|
|
239
|
+
if (rem != null) {
|
|
240
|
+
const match = closestByRem(rem, CIA_FONT_SIZES);
|
|
241
|
+
if (match) {
|
|
242
|
+
mappings[`font-size-${match.key}`] = {
|
|
243
|
+
value: `${match.value}rem`,
|
|
244
|
+
source: sourceLabel,
|
|
245
|
+
confidence: match.confidence,
|
|
246
|
+
rationale: `Bootstrap '\$${name}' (${rem}rem) → cia font-size-${match.key} (${match.value}rem); delta ${match.deltaRem.toFixed(4)}rem.`,
|
|
247
|
+
};
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 4) line-height-base → cia line-height-4 (1.5 by convention)
|
|
254
|
+
if (name === 'line-height-base') {
|
|
255
|
+
// Bootstrap default is 1.5; cia line-height-4 is 1.5
|
|
256
|
+
unmapped.push({
|
|
257
|
+
source: sourceLabel,
|
|
258
|
+
value,
|
|
259
|
+
reason: `'\$line-height-base' has no themable cia token — cia uses --line-height-* tokens (4 = 1.5 by default). Override --line-height-4 in your theme if needed.`,
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5) Utility colors ($light, $dark, $white, $black, $gray-*)
|
|
265
|
+
if (BOOTSTRAP_UTILITY_COLORS.has(name) || /^gray-\d+$/.test(name)) {
|
|
266
|
+
unmapped.push({
|
|
267
|
+
source: sourceLabel,
|
|
268
|
+
value,
|
|
269
|
+
reason: `Bootstrap utility color '\$${name}' has no direct cia semantic. cia uses --paper / --ink / --text-* / --surface-* token families. Add as a brand-* token if needed.`,
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 6) Catch-all for "I see this is a variable but I don't know where it goes"
|
|
275
|
+
// We only surface unmapped entries for variables that LOOK like they
|
|
276
|
+
// matter (border, font, color, spacer-like names) — skip the long tail
|
|
277
|
+
// of Bootstrap's internal config knobs.
|
|
278
|
+
if (/border|radius|font|color|spacer|margin|padding|shadow|width|height/i.test(name)) {
|
|
279
|
+
unmapped.push({
|
|
280
|
+
source: sourceLabel,
|
|
281
|
+
value,
|
|
282
|
+
reason: `No cia semantic mapping for Bootstrap '\$${name}'. If it's a brand/design knob you care about, add as a brand-* token or override the closest cia token manually.`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build confidence counts
|
|
288
|
+
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0, UNMAPPED: 0 };
|
|
289
|
+
for (const m of Object.values(mappings)) {
|
|
290
|
+
if (counts[m.confidence] != null) counts[m.confidence] += 1;
|
|
291
|
+
}
|
|
292
|
+
counts.UNMAPPED = unmapped.length;
|
|
293
|
+
counts.total = counts.HIGH + counts.MEDIUM + counts.LOW + counts.UNMAPPED;
|
|
294
|
+
|
|
295
|
+
return { mappings, unmapped, report: counts };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── CLI entry ────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
const HELP = `cia migrate bootstrap — convert a Bootstrap _variables.scss to a cia theme.scss
|
|
301
|
+
|
|
302
|
+
Usage:
|
|
303
|
+
cia migrate bootstrap <path> [options]
|
|
304
|
+
|
|
305
|
+
Default behavior:
|
|
306
|
+
Parses a Bootstrap SCSS variables file, maps known Bootstrap variables
|
|
307
|
+
($primary, $body-bg, $border-radius, etc.) to cia contract tokens with
|
|
308
|
+
confidence scoring, and writes a cia theme.scss to ./cia-themes/<name>.scss.
|
|
309
|
+
Diagnostics print to stderr.
|
|
310
|
+
|
|
311
|
+
Arguments:
|
|
312
|
+
path Path to a SCSS variables file (commonly _variables.scss
|
|
313
|
+
or your project's custom-variables.scss).
|
|
314
|
+
|
|
315
|
+
Options:
|
|
316
|
+
--name <name> Theme name to use in @include m.theme('<name>') and as
|
|
317
|
+
the default filename. Default: migrated
|
|
318
|
+
--out <path> Write to this path instead of the default location.
|
|
319
|
+
--json Skip the file write and dump the full JSON (vars + cia
|
|
320
|
+
mappings + unmapped + report) to stdout. Useful for
|
|
321
|
+
scripts. Diagnostics still print to stderr.
|
|
322
|
+
-h, --help Show this help.
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
cia migrate bootstrap ./scss/_variables.scss
|
|
326
|
+
cia migrate bootstrap ./scss/_variables.scss --name brand
|
|
327
|
+
cia migrate bootstrap ./scss/_variables.scss --out ./scss/themes/brand.scss
|
|
328
|
+
cia migrate bootstrap ./scss/_variables.scss --json > bootstrap-report.json
|
|
329
|
+
|
|
330
|
+
Notes:
|
|
331
|
+
- Variables that reference other Sass variables (e.g. \$primary: \$blue;)
|
|
332
|
+
can't be resolved without compiling — those surface in UNMAPPED with a
|
|
333
|
+
note to paste the literal value and re-run.
|
|
334
|
+
- Bootstrap utility colors (\$light, \$dark, \$gray-*) don't have direct
|
|
335
|
+
cia analogs and surface in UNMAPPED with brand-* token suggestions.
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
async function run(args) {
|
|
339
|
+
const { flags, positional } = parseFlags(args);
|
|
340
|
+
if (flags.help) {
|
|
341
|
+
process.stdout.write(HELP);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let varsPath = positional[0];
|
|
346
|
+
if (!varsPath) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`cia migrate bootstrap requires a path to a SCSS variables file. ` +
|
|
349
|
+
`Example: cia migrate bootstrap ./scss/_variables.scss`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (!path.isAbsolute(varsPath)) {
|
|
353
|
+
varsPath = path.resolve(process.cwd(), varsPath);
|
|
354
|
+
}
|
|
355
|
+
if (!fs.existsSync(varsPath)) {
|
|
356
|
+
throw new Error(`variables file not found: ${varsPath}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
process.stderr.write(`cia migrate bootstrap\n`);
|
|
360
|
+
process.stderr.write(` variables: ${varsPath}\n`);
|
|
361
|
+
process.stderr.write(` parsing...\n`);
|
|
362
|
+
|
|
363
|
+
const scssText = fs.readFileSync(varsPath, 'utf8');
|
|
364
|
+
const { vars, meta } = parseBootstrapVars(scssText);
|
|
365
|
+
|
|
366
|
+
const varCount = Object.keys(vars).length;
|
|
367
|
+
const defaultCount = Object.values(meta).filter((m) => m.isDefault).length;
|
|
368
|
+
process.stderr.write(` parsed ${varCount} variable(s) (${defaultCount} with !default).\n`);
|
|
369
|
+
|
|
370
|
+
process.stderr.write(`\n mapping to cia contract tokens...\n`);
|
|
371
|
+
const cia = mapBootstrapToCia(vars, meta);
|
|
372
|
+
|
|
373
|
+
process.stderr.write(`\n ─── confidence report ─────────────────────────\n`);
|
|
374
|
+
process.stderr.write(` HIGH (semantic match): ${cia.report.HIGH}\n`);
|
|
375
|
+
process.stderr.write(` MEDIUM (close, ≤0.125rem): ${cia.report.MEDIUM}\n`);
|
|
376
|
+
process.stderr.write(` LOW (best guess): ${cia.report.LOW}\n`);
|
|
377
|
+
process.stderr.write(` UNMAPPED (no cia analog): ${cia.report.UNMAPPED}\n`);
|
|
378
|
+
process.stderr.write(` ─────────────────────────────────────────────\n`);
|
|
379
|
+
process.stderr.write(` total ${cia.report.total}\n`);
|
|
380
|
+
|
|
381
|
+
// ── Output: file (default) or JSON (--json) ──
|
|
382
|
+
if (flags.json) {
|
|
383
|
+
if (cia.report.UNMAPPED > 0) {
|
|
384
|
+
process.stderr.write(`\n ${cia.report.UNMAPPED} unmapped value(s) — see .cia.unmapped in the JSON below\n`);
|
|
385
|
+
}
|
|
386
|
+
process.stderr.write(`\n --- JSON dump on stdout below ---\n`);
|
|
387
|
+
const output = {
|
|
388
|
+
source: varsPath,
|
|
389
|
+
cia_cli_phase: 'PR 4 — Bootstrap parse + map + write',
|
|
390
|
+
summary: { variables: varCount, defaults: defaultCount },
|
|
391
|
+
vars,
|
|
392
|
+
cia,
|
|
393
|
+
};
|
|
394
|
+
process.stdout.write(JSON.stringify(output, null, 2));
|
|
395
|
+
process.stdout.write('\n');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Write the theme.scss
|
|
400
|
+
const scss = writeThemeScss(cia, { name: flags.name, source: varsPath, tool: 'bootstrap' });
|
|
401
|
+
let outPath = flags.out;
|
|
402
|
+
if (outPath) {
|
|
403
|
+
if (!path.isAbsolute(outPath)) outPath = path.resolve(process.cwd(), outPath);
|
|
404
|
+
} else {
|
|
405
|
+
outPath = path.join(process.cwd(), 'cia-themes', `${flags.name}.scss`);
|
|
406
|
+
}
|
|
407
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
408
|
+
fs.writeFileSync(outPath, scss, 'utf8');
|
|
409
|
+
|
|
410
|
+
process.stderr.write(`\n wrote ${outPath}\n`);
|
|
411
|
+
process.stderr.write(` ${scss.split('\n').length - 1} lines, ${Object.keys(cia.mappings).length} mapped tokens\n`);
|
|
412
|
+
if (cia.report.UNMAPPED > 0) {
|
|
413
|
+
process.stderr.write(` ${cia.report.UNMAPPED} unmapped value(s) appended as /* */ comment block — review + add manually\n`);
|
|
414
|
+
}
|
|
415
|
+
process.stderr.write(`\n Next steps:\n`);
|
|
416
|
+
process.stderr.write(` 1. Review the file (LOW/MEDIUM/UNMAPPED entries are flagged)\n`);
|
|
417
|
+
process.stderr.write(` 2. @use this file from your app's SCSS entry\n`);
|
|
418
|
+
process.stderr.write(` 3. Set <html data-theme="${flags.name}"> and run npm run build:css\n`);
|
|
419
|
+
process.stderr.write(` 4. Run npm run validate-themes to confirm WCAG 2.2 AA contrast\n`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = {
|
|
423
|
+
run,
|
|
424
|
+
parseBootstrapVars,
|
|
425
|
+
mapBootstrapToCia,
|
|
426
|
+
BOOTSTRAP_DIRECT_MAP,
|
|
427
|
+
};
|