@ivanalbizu/astro-contrast 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ivanalbizu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # astro-contrast
2
+
3
+ WCAG color contrast analyzer for Astro components. Checks your `.astro` files for color contrast issues at build time, dev time, or on demand via CLI.
4
+
5
+ ## Features
6
+
7
+ - Parses `.astro` files and extracts color pairs (text color + background)
8
+ - Calculates WCAG 2.1 contrast ratios
9
+ - Supports hex, rgb, hsl, oklch, oklab, lab, lch, and named CSS colors (including alpha channels)
10
+ - **Large text detection** — applies lower WCAG thresholds for headings, large fonts, and bold text
11
+ - **Tailwind CSS support** — resolves `text-*` and `bg-*` utility classes
12
+ - Resolves CSS custom properties (`var(--color)`) from `:root`
13
+ - **Auto-detects external CSS** — `<link rel="stylesheet">` and `@import` are loaded automatically
14
+ - Reads external design tokens (Style Dictionary, Cobalt UI, Terrazzo)
15
+ - Ignore specific elements or rules with comments
16
+ - **GitHub Actions annotations** — contrast failures appear inline in PR diffs automatically
17
+ - **Dev dashboard** — visual contrast report in the browser during development
18
+ - Works as **CLI**, **Astro integration**, or **programmatic API**
19
+ - Watch mode for real-time analysis during development
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install @ivanalbizu/astro-contrast
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### CLI
30
+
31
+ ```sh
32
+ # Analyze all .astro files
33
+ astro-contrast "src/**/*.astro"
34
+
35
+ # With WCAG AAA level
36
+ astro-contrast "src/**/*.astro" --level aaa
37
+
38
+ # Show all pairs, not just failures
39
+ astro-contrast "src/**/*.astro" --verbose
40
+
41
+ # Watch mode
42
+ astro-contrast "src/**/*.astro" --watch
43
+
44
+ # With design tokens
45
+ astro-contrast "src/**/*.astro" --tokens tokens/colors.json
46
+ astro-contrast "src/**/*.astro" --tokens primitives.json --tokens semantic.json
47
+
48
+ # With external/global CSS
49
+ astro-contrast "src/**/*.astro" --css src/styles/global.css
50
+ astro-contrast "src/**/*.astro" --css variables.css --css theme.css
51
+
52
+ # JSON output
53
+ astro-contrast "src/**/*.astro" --json
54
+ ```
55
+
56
+ #### CLI Options
57
+
58
+ | Option | Description |
59
+ |---|---|
60
+ | `--level aa\|aaa` | WCAG level to check (default: `aa`) |
61
+ | `--verbose`, `-v` | Show all pairs, not just failures |
62
+ | `--watch`, `-w` | Watch for file changes and re-analyze |
63
+ | `--tokens <file>` | Token file (JSON/YAML/CSS). Repeatable |
64
+ | `--css <file>` | External CSS file. Repeatable |
65
+ | `--ignore-color <val>` | Color to ignore globally. Repeatable |
66
+ | `--ignore-pair <val>` | Color pair to ignore (`fg:bg`). Repeatable |
67
+ | `--ignore-selector <sel>` | Selector to ignore (`.class`, `#id`, `tag`, `.prefix-*`). Repeatable |
68
+ | `--json` | Output results as JSON |
69
+ | `--help`, `-h` | Show help |
70
+ | `--version` | Show version |
71
+
72
+ #### Exit codes
73
+
74
+ | Code | Meaning |
75
+ |---|---|
76
+ | `0` | All pairs pass the required level |
77
+ | `1` | At least one pair fails |
78
+ | `2` | Error during analysis |
79
+
80
+ ### Astro Integration
81
+
82
+ Add `@ivanalbizu/astro-contrast` to your `astro.config.mjs`:
83
+
84
+ ```js
85
+ import { defineConfig } from 'astro/config';
86
+ import astroContrast from '@ivanalbizu/astro-contrast';
87
+
88
+ export default defineConfig({
89
+ integrations: [
90
+ astroContrast({
91
+ level: 'aa',
92
+ verbose: true,
93
+ css: ['src/styles/global.css'],
94
+ }),
95
+ ],
96
+ });
97
+ ```
98
+
99
+ This runs contrast analysis automatically during `astro dev` (with file watching) and `astro build`.
100
+
101
+ #### Integration Options
102
+
103
+ | Option | Type | Default | Description |
104
+ |---|---|---|---|
105
+ | `level` | `'aa' \| 'aaa'` | `'aa'` | WCAG level to check |
106
+ | `verbose` | `boolean` | `false` | Show all pairs, not just failures |
107
+ | `failOnError` | `boolean` | `false` | Fail the build if contrast issues are found |
108
+ | `tokens` | `string[]` | `[]` | Paths to token files |
109
+ | `css` | `string[]` | `[]` | Paths to external CSS files |
110
+ | `ignore` | `IgnoreConfig` | `undefined` | Global ignore rules (see below) |
111
+ | `dashboard` | `boolean \| string` | `false` | Dev dashboard URL. `true` = `/_contrast`, string = custom path |
112
+
113
+ ### Dev Dashboard
114
+
115
+ Enable the dashboard to view contrast results in the browser during development:
116
+
117
+ ```js
118
+ astroContrast({
119
+ dashboard: true, // available at http://localhost:4321/_contrast
120
+ })
121
+ ```
122
+
123
+ Or with a custom URL:
124
+
125
+ ```js
126
+ astroContrast({
127
+ dashboard: '/debug-astro-contrast',
128
+ })
129
+ ```
130
+
131
+ The dashboard shows all analyzed files with their color pairs, contrast ratios, and pass/fail status. It auto-refreshes every 2 seconds when you edit `.astro` files — no need to reload the page.
132
+
133
+ A JSON API is also available at `{dashboardUrl}/api` for programmatic access.
134
+
135
+ ### Programmatic API
136
+
137
+ ```ts
138
+ import { analyzeFile, analyzeFiles } from '@ivanalbizu/astro-contrast';
139
+
140
+ const result = await analyzeFile('src/components/Button.astro');
141
+
142
+ console.log(result.stats);
143
+ // { elementsAnalyzed: 2, pairsChecked: 2, passing: 1, aaFailing: 1, ... }
144
+
145
+ for (const cr of result.results) {
146
+ console.log(`${cr.element.tagName} — ${cr.ratio}:1 — ${cr.meetsAA ? 'PASS' : 'FAIL'}`);
147
+ }
148
+ ```
149
+
150
+ With external tokens:
151
+
152
+ ```ts
153
+ import { analyzeFiles } from '@ivanalbizu/astro-contrast';
154
+ import { readTokenFiles } from 'astro-contrast/dist/chunk-HPYGY3PW.js'; // internal
155
+
156
+ const tokens = await readTokenFiles(['tokens/colors.json']);
157
+ const results = await analyzeFiles(files, { externalTokens: tokens });
158
+ ```
159
+
160
+ ## Design Tokens
161
+
162
+ astro-contrast can read external design token files and resolve `var()` references against them. Supported formats:
163
+
164
+ ### JSON — W3C DTCG / Style Dictionary
165
+
166
+ Compatible with [W3C Design Token Community Group](https://tr.designtokens.org/format/) format, used by Cobalt UI, Terrazzo, and Style Dictionary v4:
167
+
168
+ ```json
169
+ {
170
+ "color": {
171
+ "$type": "color",
172
+ "primary": {
173
+ "$value": "#1a5276"
174
+ },
175
+ "danger": {
176
+ "$value": "#e74c3c"
177
+ },
178
+ "info": {
179
+ "$value": "{color.primary}"
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ Also supports Style Dictionary v3 format (`value`/`type` without `$` prefix).
186
+
187
+ ### YAML
188
+
189
+ Same structure as JSON, with `.yaml` or `.yml` extension:
190
+
191
+ ```yaml
192
+ color:
193
+ $type: color
194
+ primary:
195
+ $value: "#1a5276"
196
+ danger:
197
+ $value: "#e74c3c"
198
+ info:
199
+ $value: "{color.primary}"
200
+ ```
201
+
202
+ ### CSS
203
+
204
+ Plain CSS files with `:root` declarations:
205
+
206
+ ```css
207
+ :root {
208
+ --color-primary: #1a5276;
209
+ --color-danger: #e74c3c;
210
+ }
211
+ ```
212
+
213
+ ### How tokens are mapped
214
+
215
+ Token paths are converted to CSS custom properties:
216
+
217
+ | Token path | CSS variable |
218
+ |---|---|
219
+ | `color.primary` | `--color-primary` |
220
+ | `color.primitives.blue.500` | `--color-primitives-blue-500` |
221
+
222
+ Token references (`{color.primary}`) are resolved recursively before mapping.
223
+
224
+ When a `.astro` file defines the same custom property in its `<style>` block, the in-file value takes priority over the external token.
225
+
226
+ ## External / Global CSS
227
+
228
+ ### Auto-detection
229
+
230
+ astro-contrast automatically detects and loads external CSS referenced in your `.astro` files via:
231
+
232
+ - **`import "./styles.css"`** — in the frontmatter (component script)
233
+ - **`<link rel="stylesheet" href="./styles.css">`** — in the HTML template
234
+ - **`@import './tokens.css'`** — inside `<style>` blocks
235
+
236
+ This works recursively: if `a.css` imports `b.css` which imports `c.css`, all three are loaded. Circular imports are handled safely.
237
+
238
+ Only relative paths (`./`, `../`) are resolved. Absolute URLs (`http://`, `https://`) and package imports are skipped.
239
+
240
+ ### Manual `--css` flag
241
+
242
+ For CSS files that aren't referenced directly in `.astro` files (e.g. injected by a build tool), use `--css` (CLI) or `css` (integration):
243
+
244
+ ```sh
245
+ astro-contrast "src/**/*.astro" --css src/styles/global.css --css src/styles/variables.css
246
+ ```
247
+
248
+ External CSS files provide:
249
+ - **CSS rules** — selectors and declarations (e.g. `.heading { color: #1a5276; }`)
250
+ - **Custom properties** — `:root` variables (e.g. `--color-primary: #1a5276`)
251
+
252
+ **Priority order**: external tokens < auto-detected CSS < manual `--css` < in-file `:root` / `<style>`. In-file styles always win at equal specificity.
253
+
254
+ ## Tailwind CSS
255
+
256
+ astro-contrast detects Tailwind utility classes and resolves them to colors using the default Tailwind v3 palette. No configuration needed.
257
+
258
+ **Supported patterns:**
259
+
260
+ ```astro
261
+ <!-- Standard palette colors -->
262
+ <button class="text-white bg-blue-700">Submit</button>
263
+
264
+ <!-- Arbitrary values -->
265
+ <p class="text-[#1a5276] bg-[#d4e6f1]">Custom colors</p>
266
+ ```
267
+
268
+ | Pattern | Example | Resolves to |
269
+ |---|---|---|
270
+ | `text-{color}-{shade}` | `text-blue-500` | `color: #3b82f6` |
271
+ | `bg-{color}-{shade}` | `bg-red-600` | `background-color: #dc2626` |
272
+ | `text-white` / `text-black` | `text-white` | `color: #ffffff` |
273
+ | `text-[value]` | `text-[#1a5276]` | `color: #1a5276` |
274
+ | `bg-[value]` | `bg-[rgb(26,82,118)]` | `background-color: rgb(26,82,118)` |
275
+
276
+ **Priority order**: inline styles > Tailwind classes > CSS rules > defaults.
277
+
278
+ **Not yet supported**: responsive variants (`md:text-white`), state variants (`hover:bg-blue-500`), opacity modifiers (`text-blue-500/50`), custom Tailwind config.
279
+
280
+ ## Ignoring Elements
281
+
282
+ You can skip specific elements or CSS rules from contrast analysis using ignore comments.
283
+
284
+ ### HTML comment
285
+
286
+ Place `<!-- astro-contrast-ignore -->` before the element to skip:
287
+
288
+ ```astro
289
+ <p class="decorative">This is checked</p>
290
+ <!-- astro-contrast-ignore -->
291
+ <p class="decorative">This is skipped</p>
292
+ ```
293
+
294
+ ### HTML attribute
295
+
296
+ Add `data-contrast-ignore` to the element:
297
+
298
+ ```astro
299
+ <span class="badge" data-contrast-ignore>Decorative badge</span>
300
+ ```
301
+
302
+ ### CSS comment
303
+
304
+ Place `/* astro-contrast-ignore */` before a CSS rule to skip it:
305
+
306
+ ```css
307
+ .alert {
308
+ color: #fff;
309
+ background-color: #e74c3c;
310
+ }
311
+ /* astro-contrast-ignore */
312
+ .decorative {
313
+ color: #ccc;
314
+ background-color: #ddd;
315
+ }
316
+ ```
317
+
318
+ ### Global Ignore Rules
319
+
320
+ For repetitive patterns like brand colors that intentionally fail contrast, use global ignore rules instead of per-element comments.
321
+
322
+ #### Astro Integration
323
+
324
+ ```js
325
+ astroContrast({
326
+ ignore: {
327
+ colors: ['#e74c3c'], // ignore this color everywhere
328
+ pairs: [{ foreground: '#ffffff', background: '#e74c3c' }], // ignore this exact pair
329
+ selectors: ['.brand-badge', '.alert-*'], // ignore matching elements
330
+ }
331
+ })
332
+ ```
333
+
334
+ #### CLI
335
+
336
+ ```sh
337
+ # Ignore a color globally (repeatable)
338
+ astro-contrast "src/**/*.astro" --ignore-color "#e74c3c"
339
+
340
+ # Ignore a specific foreground:background pair (repeatable)
341
+ astro-contrast "src/**/*.astro" --ignore-pair "#ffffff:#e74c3c"
342
+
343
+ # Ignore elements by selector, supports * wildcards (repeatable)
344
+ astro-contrast "src/**/*.astro" --ignore-selector ".brand-badge" --ignore-selector ".alert-*"
345
+ ```
346
+
347
+ | Rule type | What it does |
348
+ |---|---|
349
+ | `colors` | Ignores any pair where the color appears as foreground **or** background |
350
+ | `pairs` | Ignores only when both foreground and background match exactly |
351
+ | `selectors` | Ignores elements matching `.class`, `#id`, `tag`, or wildcard patterns like `.prefix-*` |
352
+
353
+ Colors are compared by their resolved RGB value — `#e74c3c` and `rgb(231, 76, 60)` are treated as the same color.
354
+
355
+ ## CI / GitHub Actions
356
+
357
+ When running in GitHub Actions, astro-contrast automatically emits annotations that appear inline in pull request diffs. No configuration needed — it detects the `GITHUB_ACTIONS` environment variable.
358
+
359
+ Contrast failures appear as error annotations on the exact line of the failing element.
360
+
361
+ **Example workflow:**
362
+
363
+ ```yaml
364
+ - name: Check contrast
365
+ run: astro-contrast "src/**/*.astro"
366
+ ```
367
+
368
+ If the check fails (exit code 1), the PR will show annotations like:
369
+
370
+ > **Error** src/components/Card.astro#L7
371
+ > Contrast 2.8:1 fails AA (requires 4.5:1) — #999999 on #ffffff (.card-meta)
372
+
373
+ When using `--level aaa`, pairs that pass AA but fail AAA appear as warnings instead of errors.
374
+
375
+ ## How It Works
376
+
377
+ 1. **Parse** — Reads `.astro` files using `@astrojs/compiler` and extracts HTML elements, `<style>` blocks, `<link>` hrefs, and `@import` references
378
+ 2. **Extract CSS** — Parses style blocks with PostCSS to get selectors, color declarations, and `:root` custom properties. Auto-loads CSS from `<link>` and `@import` recursively
379
+ 3. **Resolve** — Resolves `var()` references using custom properties from `:root`, auto-detected CSS, external CSS files, and/or design tokens
380
+ 4. **Match** — Matches HTML elements to CSS rules by selector (type, class, ID, compound) and resolves Tailwind utility classes
381
+ 5. **Evaluate** — Calculates WCAG 2.1 contrast ratio for each foreground/background pair
382
+ 6. **Report** — Outputs results with pass/fail status for AA and AAA levels
383
+
384
+ ## WCAG 2.1 Contrast Requirements
385
+
386
+ | Level | Normal text | Large text |
387
+ |---|---|---|
388
+ | **AA** | 4.5:1 | 3:1 |
389
+ | **AAA** | 7:1 | 4.5:1 |
390
+
391
+ > **Large text** is defined as ≥ 18px, or ≥ 14px bold (font-weight ≥ 700). astro-contrast detects font size from inline styles, CSS rules, Tailwind classes (`text-xl`, `font-bold`, etc.), and HTML heading defaults (`<h1>`–`<h4>`).
392
+ >
393
+ > Viewport-dependent values (`vw`, `vh`) and CSS functions (`clamp()`, `min()`, `max()`, `calc()`) cannot be resolved statically. When the font size cannot be determined, astro-contrast defaults to **normal text** thresholds — the stricter requirement — to avoid false passes.
394
+
395
+ ## Example Output
396
+
397
+ ```
398
+ src/components/atoms/Button.astro
399
+ PASS .btn-primary L5 #ffffff on #1a5276 → 8.4:1 (AA ✓ AAA ✓)
400
+ PASS .btn-danger L6 #ffffff on #514f4f → 8.1:1 (AA ✓ AAA ✓)
401
+
402
+ src/components/molecules/Card.astro
403
+ PASS .card-title L6 #1a1a1a on #ffffff → 17.4:1 (AA ✓ AAA ✓) [large]
404
+ FAIL .card-meta L7 #999999 on #ffffff → 2.8:1 (AA requires 4.5:1)
405
+ PASS .card-body L8 #333333 on #ffffff → 12.6:1 (AA ✓ AAA ✓)
406
+
407
+ Summary:
408
+ Files analyzed: 2
409
+ Color pairs checked: 5
410
+ Passing: 4 | Failing: 1
411
+ ```
412
+
413
+ ## Development Scripts
414
+
415
+ | Script | Description |
416
+ |---|---|
417
+ | `npm run build` | Build with tsup |
418
+ | `npm run dev` | Build in watch mode |
419
+ | `npm test` | Run tests |
420
+ | `npm run test:watch` | Tests in watch mode |
421
+ | `npm run typecheck` | TypeScript check (`tsc --noEmit`) |
422
+ | `npm run prepublishOnly` | Runs typecheck + test + build (auto on `npm publish`) |
423
+
424
+ ## Current Limitations
425
+
426
+ - **Background inheritance (same file)** — Child elements inherit `background-color` from ancestor elements within the same `.astro` file (inline styles, Tailwind classes, and CSS rules). Cross-file inheritance (e.g. layout → page) requires `--css`
427
+ - **No pseudo-classes** — `:hover`, `:focus`, `:active` states are not analyzed. Only base-state selectors are matched (planned for a future release)
428
+ - **Simple selectors only** — Supports type, class, ID, and compound selectors. No combinators or media queries
429
+ - **No dynamic font sizes** — `clamp()`, `min()`, `max()`, `calc()`, and viewport units (`vw`, `vh`) cannot be resolved; text is treated as normal size (stricter threshold)
430
+ - **No SCSS/SASS/Less** — Only plain CSS in `<style>` blocks is supported. Preprocessor syntax (`$variables`, nesting, `@mixin`) is not parsed. Colors defined via preprocessors can still be analyzed if compiled to CSS custom properties and loaded with `--css`
431
+ - **Partial alpha compositing** — Foreground colors with alpha (`rgba`, `hsla`, hex8) are composited onto the background. Background alpha, CSS `opacity`, and nested opacity require CSS inheritance and are not supported
432
+ - **No dark mode / theme scopes** — Only `:root` and `html` custom properties are extracted. Variables under `[data-theme="dark"]`, `.dark`, or `@media (prefers-color-scheme: dark)` are not resolved. Workaround: use separate CSS files per theme and run with `--css themes/light.css` or `--css themes/dark.css`
433
+
434
+ ## License
435
+
436
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js').then(m => m.run());