emily-css 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -69
- package/README.md +60 -111
- package/bin/emilyui.js +6 -1
- package/package.json +3 -3
- package/src/doctor.js +119 -4
- package/src/generators/accessibility.js +40 -0
- package/src/generators/animation.js +31 -0
- package/src/generators/background.js +36 -0
- package/src/generators/code.js +31 -0
- package/src/generators/display.js +46 -0
- package/src/generators/effects.js +218 -0
- package/src/generators/forms.js +25 -0
- package/src/generators/index.js +45 -0
- package/src/generators/layout.js +138 -0
- package/src/generators/overflow.js +36 -0
- package/src/generators/positioning.js +58 -0
- package/src/generators/rings.js +41 -0
- package/src/generators/sizing.js +111 -0
- package/src/generators/svg.js +38 -0
- package/src/generators/transforms.js +61 -0
- package/src/generators.js +2 -884
- package/src/index.js +78 -5
- package/src/intellisense.js +27 -0
- package/src/manifest.js +90 -24
- package/src/purge.js +40 -20
- package/templates/showcase.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,82 +1,140 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `emily-css` are documented here.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
## v1.2.0-alpha.0 — May 2026
|
|
7
|
-
|
|
8
|
-
### Added
|
|
9
|
-
- Report-only Tailwind-to-EmilyCSS migration command: `emily-css migrate`.
|
|
10
|
-
- Default semantic migration mode for design-token aligned suggestions.
|
|
11
|
-
- Imported palette mode via `emily-css migrate --import-colours` for visual parity mapping suggestions.
|
|
12
|
-
- Detection and reporting for arbitrary value utilities during migration analysis.
|
|
13
|
-
|
|
14
|
-
### Notes
|
|
15
|
-
- Migration in this alpha is analysis-only: no source files are modified by `migrate`.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
## v1.1.0 — May 2026
|
|
19
|
-
|
|
20
|
-
### Added
|
|
21
|
-
- Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
|
|
22
|
-
- Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
|
|
23
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `emily-css` are documented here.
|
|
4
|
+
|
|
24
5
|
---
|
|
25
6
|
|
|
26
|
-
## v1.2.
|
|
27
|
-
|
|
28
|
-
**updated the full system to be more efficient**
|
|
7
|
+
## v1.2.2 — May 2026
|
|
29
8
|
|
|
30
9
|
### Added
|
|
31
|
-
-
|
|
10
|
+
- Added IntelliSense JSON generation via `intellisense` config output (`dist/emily.intellisense.json` by default).
|
|
11
|
+
- Added build profiling via `emily-css build --profile` with coarse timing buckets.
|
|
12
|
+
- Added initial accessibility warnings to `emily-css doctor` (focus removal, same token text/background, and `cursor-pointer` on non-interactive elements).
|
|
13
|
+
- Added documentation stubs in `docs/` for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
|
|
32
14
|
|
|
33
15
|
### Changed
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## v1.1.1 — May 2026
|
|
38
|
-
|
|
39
|
-
**updated changes and added**
|
|
40
|
-
|
|
41
|
-
### Added
|
|
42
|
-
- updted changes
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
## v1.1.0 — May 2026
|
|
46
|
-
|
|
47
|
-
**add utility manifest generation): chore: release v1.1.0**
|
|
48
|
-
|
|
49
|
-
### Added
|
|
50
|
-
- add utility manifest generation): chore: release v1.1.0
|
|
16
|
+
- Stabilised manifest schema metadata with explicit `schemaVersion`, package name, and package version fields.
|
|
17
|
+
- Improved purge class extraction for complex variant patterns and safer junk filtering.
|
|
18
|
+
- Updated README to reflect current product direction and command surface.
|
|
51
19
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
**added json manifest for future use**
|
|
56
|
-
|
|
57
|
-
### Added
|
|
58
|
-
- added json manifest for future use
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
## v1.0.28 — May 2026
|
|
62
|
-
|
|
63
|
-
**added new utilities**
|
|
64
|
-
|
|
65
|
-
### Changed
|
|
66
|
-
- added new utilties and tests
|
|
20
|
+
### Notes
|
|
21
|
+
- EmilyCSS remains CommonJS-compatible and continues to support Node 16+.
|
|
22
|
+
- ESM-only dependency major upgrades remain intentionally deferred for compatibility.
|
|
67
23
|
|
|
68
24
|
---
|
|
69
|
-
## v1.0.27 — May 2026
|
|
70
|
-
|
|
71
|
-
**colour system redesign — brand/accent tokens + semantic colours (v1.0.23)**
|
|
72
25
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
26
|
+
## v1.2.1 — May 2026
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Refactored utility generators into smaller internal modules.
|
|
30
|
+
- Kept `src/generators.js` as a compatibility shim so existing imports continue to work.
|
|
31
|
+
- Moved shared defaults into `src/constants.js`.
|
|
32
|
+
- Updated watch mode so configured purge ignore rules are respected consistently.
|
|
33
|
+
- Improved release hardening and package output checks.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Kept dependency versions on CommonJS-compatible majors to support Node 16+.
|
|
37
|
+
- Avoided ESM-only dependency upgrades that would break the current CommonJS CLI.
|
|
38
|
+
|
|
39
|
+
### Dependency compatibility
|
|
40
|
+
EmilyCSS intentionally stays on CommonJS-compatible dependency majors for now:
|
|
41
|
+
|
|
42
|
+
- `chalk@4`
|
|
43
|
+
- `ora@5`
|
|
44
|
+
- `boxen@5`
|
|
45
|
+
- `chokidar@4`
|
|
46
|
+
|
|
47
|
+
Newer major versions are ESM-focused and may require newer Node versions. EmilyCSS currently supports Node 16+ and CommonJS.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## v1.2.0-alpha.0 — May 2026
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- Report-only Tailwind-to-EmilyCSS migration command: `emily-css migrate`.
|
|
55
|
+
- Default semantic migration mode for design-token aligned suggestions.
|
|
56
|
+
- Imported palette mode via `emily-css migrate --import-colours` for visual parity mapping suggestions.
|
|
57
|
+
- Detection and reporting for arbitrary value utilities during migration analysis.
|
|
58
|
+
|
|
59
|
+
### Notes
|
|
60
|
+
- Migration in this alpha is analysis-only. No source files are modified by `migrate`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## v1.1.1 — May 2026
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- Utility manifest generation for future tooling, doctor checks, migration support, and editor integrations.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## v1.1.0 — May 2026
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
- Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
|
|
75
|
+
- Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## v1.2.1 — May 2026
|
|
80
|
+
|
|
81
|
+
**Refactor utility generators into modules**
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
## v1.2.1 — May 2026
|
|
85
|
+
|
|
86
|
+
**updated the full system to be more efficient**
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
- updated the full system to be more efficient
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
- updated release logic
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
## v1.1.1 — May 2026
|
|
96
|
+
|
|
97
|
+
**updated changes and added**
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
- updted changes
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
## v1.1.0 — May 2026
|
|
104
|
+
|
|
105
|
+
**add utility manifest generation): chore: release v1.1.0**
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
- add utility manifest generation): chore: release v1.1.0
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
## v1.0.29 — May 2026
|
|
112
|
+
|
|
113
|
+
**added json manifest for future use**
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
- added json manifest for future use
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
## v1.0.28 — May 2026
|
|
120
|
+
|
|
121
|
+
**added new utilities**
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
- added new utilties and tests
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
## v1.0.27 — May 2026
|
|
128
|
+
|
|
129
|
+
**colour system redesign — brand/accent tokens + semantic colours (v1.0.23)**
|
|
130
|
+
|
|
131
|
+
### Added
|
|
132
|
+
- colour system redesign — brand/accent tokens + semantic colours (v1.0.23)
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
- updated utilties and new showcase
|
|
136
|
+
|
|
137
|
+
---
|
|
80
138
|
# Changelog
|
|
81
139
|
|
|
82
140
|
All notable changes to `emily-css` are documented here.
|
package/README.md
CHANGED
|
@@ -1,158 +1,107 @@
|
|
|
1
1
|
# emilyCSS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Token-first, framework-agnostic CSS generation for teams that want predictable utilities without adopting a full app framework.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What emilyCSS is
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
emilyCSS lets you define design tokens in `emily.config.json` and generate static CSS you can use anywhere: HTML, Drupal, WordPress, Power Pages, React, Vue, and other environments.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Why teams use it
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- Token-first utility generation from your own colours, spacing, typography, and motion settings
|
|
12
|
+
- Framework-agnostic output (`dist/emily.css` and `dist/emily.min.css`)
|
|
13
|
+
- Accessibility-focused utility coverage (focus rings, visually-hidden helpers, motion-aware variants)
|
|
14
|
+
- Tooling support with manifest and IntelliSense JSON generation
|
|
15
|
+
- CommonJS package with Node 16+ compatibility
|
|
14
16
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
### 1. Initialize
|
|
17
|
+
## Install and basic workflow
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
+
npm install emily-css
|
|
20
21
|
npx emily-css init
|
|
22
|
+
npx emily-css build
|
|
23
|
+
npx emily-css watch
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### 2. Link the CSS
|
|
26
|
+
Link production CSS in your project:
|
|
26
27
|
|
|
27
28
|
```html
|
|
28
29
|
<link rel="stylesheet" href="./dist/emily.min.css">
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Use the generated utilities and browse the showcase for ready-to-copy components.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npx emily-css build # Rebuild after config changes
|
|
37
|
-
npx emily-css watch # Watch mode for development
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Core Features
|
|
41
|
-
|
|
42
|
-
- **Token-Driven Colours** — One hex per colour → balanced 10-shade scale using OKLCH
|
|
43
|
-
- **Predictable Spacing** — Everything scales from your baseUnit
|
|
44
|
-
- **Accessibility First** — Focus-visible rings, motion utilities, WCAG 2.2 AA colours
|
|
45
|
-
- **No Build Pipeline Required** — Just a static CSS file
|
|
46
|
-
- **Smart Purge** — Remove unused utilities for tiny production files
|
|
47
|
-
- **UI Starter Kit** — Copy-paste accessible components from showcase.html
|
|
48
|
-
|
|
49
|
-
## Commands
|
|
32
|
+
## Core commands
|
|
50
33
|
|
|
51
34
|
```bash
|
|
52
|
-
npx emily-css init
|
|
53
|
-
npx emily-css build
|
|
54
|
-
npx emily-css
|
|
55
|
-
npx emily-css
|
|
56
|
-
npx emily-css
|
|
57
|
-
|
|
35
|
+
npx emily-css init
|
|
36
|
+
npx emily-css build
|
|
37
|
+
npx emily-css build --profile
|
|
38
|
+
npx emily-css watch
|
|
39
|
+
npx emily-css doctor
|
|
40
|
+
npx emily-css migrate
|
|
41
|
+
npx emily-css migrate --import-colours
|
|
42
|
+
npx emily-css showcase
|
|
43
|
+
npx emily-css help
|
|
44
|
+
npx emily-css version
|
|
58
45
|
```
|
|
59
46
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
- `emily-css migrate` is report-only and does not modify files.
|
|
63
|
-
- Default migration mode is semantic (`gray/slate/zinc/stone` remap toward `neutral` naming).
|
|
64
|
-
- `emily-css migrate --import-colours` enables imported palette mode for parity-oriented palette suggestions.
|
|
65
|
-
- Arbitrary value utilities (for example `w-[37px]`, `bg-[#0f172a]`) are detected and reported as unsupported.
|
|
47
|
+
## Doctor and migrate
|
|
66
48
|
|
|
67
|
-
|
|
49
|
+
- `doctor` checks for unknown EmilyCSS classes and variants.
|
|
50
|
+
- `doctor` now also reports non-failing accessibility warnings (for example obvious focus-removal or same-token text/background patterns).
|
|
51
|
+
- `migrate` is report-only and helps plan Tailwind-to-Emily migrations without modifying files.
|
|
68
52
|
|
|
69
|
-
|
|
53
|
+
## Manifest and IntelliSense JSON
|
|
70
54
|
|
|
71
|
-
|
|
55
|
+
Enable machine-readable outputs when needed:
|
|
72
56
|
|
|
73
|
-
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"manifest": true,
|
|
60
|
+
"intellisense": {
|
|
61
|
+
"enabled": true,
|
|
62
|
+
"output": "dist/emily.intellisense.json"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
74
66
|
|
|
75
|
-
|
|
67
|
+
Generated files:
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
| Full build | ~1.1 MB |
|
|
80
|
-
| After purge | 10–50 KB |
|
|
69
|
+
- `dist/emily.manifest.json`
|
|
70
|
+
- `dist/emily.intellisense.json`
|
|
81
71
|
|
|
82
|
-
|
|
72
|
+
These files are intended for tooling, audits, and editor integrations. A VSCode extension is not required for generation.
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
## Minimal configuration example
|
|
85
75
|
|
|
86
76
|
```json
|
|
87
77
|
{
|
|
88
78
|
"name": "My Brand",
|
|
89
|
-
"baseUnit": "8px",
|
|
90
79
|
"fontFamily": {
|
|
91
|
-
"heading": "
|
|
80
|
+
"heading": "atkinson",
|
|
92
81
|
"body": "inter"
|
|
93
82
|
},
|
|
94
83
|
"colours": {
|
|
95
|
-
"brand": "#
|
|
96
|
-
"accent": "#
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
84
|
+
"brand": "#0077B6",
|
|
85
|
+
"accent": "#0EA5E9",
|
|
86
|
+
"neutral": "#57534E",
|
|
87
|
+
"success": "#0F766E",
|
|
88
|
+
"error": "#B91C1C"
|
|
100
89
|
},
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
"light": "#FAFAFA"
|
|
104
|
-
},
|
|
105
|
-
"purge": {
|
|
106
|
-
"content": ["./**/*.{html,php,jsx,tsx,vue}"]
|
|
107
|
-
}
|
|
90
|
+
"manifest": true,
|
|
91
|
+
"intellisense": true
|
|
108
92
|
}
|
|
109
93
|
```
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
## Component Showcase
|
|
114
|
-
|
|
115
|
-
After your first build, open `showcase.html` in your browser. It contains production-ready, accessible components (buttons, forms, alerts, cards, etc.) built with your brand.
|
|
116
|
-
|
|
117
|
-
## EmilyUI vs emilyCSS
|
|
118
|
-
|
|
119
|
-
- **EmilyUI** — The broader brand / ecosystem
|
|
120
|
-
- **emilyCSS** — The current product (the emily-css npm package + CLI)
|
|
121
|
-
|
|
122
|
-
## Example Components
|
|
123
|
-
|
|
124
|
-
### Button
|
|
125
|
-
|
|
126
|
-
```html
|
|
127
|
-
<button class="px-6 py-3 rounded-lg bg-brand-80 text-white hover:bg-brand-90 focus-visible:ring-2 focus-visible:ring-brand-50 font-medium">
|
|
128
|
-
Submit
|
|
129
|
-
</button>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Responsive Card
|
|
133
|
-
|
|
134
|
-
```html
|
|
135
|
-
<div class="w-full md:w-96 p-6 rounded-xl bg-white border border-neutral-20 shadow-sm">
|
|
136
|
-
<h2 class="text-2xl font-semibold text-neutral-90">Card Title</h2>
|
|
137
|
-
<p class="mt-3 text-neutral-70">Content goes here.</p>
|
|
138
|
-
</div>
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Fonts
|
|
142
|
-
|
|
143
|
-
emilyCSS applies font stacks but does not include font files. Recommended approach:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
npm install @fontsource/inter @fontsource/lexend
|
|
147
|
-
```
|
|
95
|
+
## Notes on compatibility
|
|
148
96
|
|
|
149
|
-
|
|
97
|
+
- Package format: CommonJS
|
|
98
|
+
- Runtime support: Node 16+
|
|
99
|
+
- ESM-only major upgrades are intentionally avoided where they would break compatibility
|
|
150
100
|
|
|
151
|
-
##
|
|
101
|
+
## Documentation stubs
|
|
152
102
|
|
|
153
|
-
|
|
154
|
-
- **GitHub:** https://github.com/andyjterry/emily-ui
|
|
103
|
+
Starter docs are available in [`docs/`](./docs) for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
|
|
155
104
|
|
|
156
105
|
## License
|
|
157
106
|
|
|
158
|
-
MIT
|
|
107
|
+
MIT
|
package/bin/emilyui.js
CHANGED
|
@@ -10,6 +10,7 @@ const usageText = `
|
|
|
10
10
|
Usage:
|
|
11
11
|
emily-css init Set up a new project
|
|
12
12
|
emily-css build Generate production CSS to the configured output path
|
|
13
|
+
--profile Print coarse build timing information
|
|
13
14
|
emily-css watch Dev mode: rebuild on changes
|
|
14
15
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
15
16
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
|
@@ -24,7 +25,10 @@ if (command === "init") {
|
|
|
24
25
|
require("../src/init.js");
|
|
25
26
|
} else if (command === "build") {
|
|
26
27
|
const { build } = require("../src/index.js");
|
|
27
|
-
build({
|
|
28
|
+
build({
|
|
29
|
+
keepFull: process.argv.includes("--keep-full"),
|
|
30
|
+
profile: process.argv.includes("--profile"),
|
|
31
|
+
});
|
|
28
32
|
} else if (command === "watch") {
|
|
29
33
|
require("../src/watch.js");
|
|
30
34
|
} else if (command === "showcase") {
|
|
@@ -48,6 +52,7 @@ if (command === "init") {
|
|
|
48
52
|
Commands:
|
|
49
53
|
emily-css init Set up a new project (interactive wizard)
|
|
50
54
|
emily-css build Generate production CSS to the configured output path
|
|
55
|
+
--profile Print coarse build timing information
|
|
51
56
|
emily-css watch Dev mode: watch for changes and rebuild
|
|
52
57
|
emily-css doctor Scan project files for unknown EmilyCSS classes
|
|
53
58
|
emily-css migrate Generate a Tailwind-to-EmilyCSS migration report
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emily-css",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"node": ">=16.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"nodemon": "^3.
|
|
48
|
+
"nodemon": "^3.1.14"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"boxen": "^5.1.2",
|
|
52
52
|
"chalk": "^4.1.2",
|
|
53
|
-
"chokidar": "^
|
|
53
|
+
"chokidar": "^4.0.3",
|
|
54
54
|
"cross-spawn": "^7.0.6",
|
|
55
55
|
"enquirer": "^2.4.1",
|
|
56
56
|
"fast-glob": "^3.3.3",
|
package/src/doctor.js
CHANGED
|
@@ -185,6 +185,102 @@ function loadManifest(config, css) {
|
|
|
185
185
|
return generateManifest(css, config);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
const INTERACTIVE_TAGS = new Set([
|
|
189
|
+
"a",
|
|
190
|
+
"button",
|
|
191
|
+
"input",
|
|
192
|
+
"select",
|
|
193
|
+
"textarea",
|
|
194
|
+
"summary",
|
|
195
|
+
"option",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const VISIBLE_FOCUS_CLASSES = new Set([
|
|
199
|
+
"focus-ring",
|
|
200
|
+
"focus-visible:ring-2",
|
|
201
|
+
"outline",
|
|
202
|
+
"outline-2",
|
|
203
|
+
]);
|
|
204
|
+
const KNOWN_CLASS_SHIMS = new Set(["focus-ring-none"]);
|
|
205
|
+
|
|
206
|
+
function parseElementClassLists(content) {
|
|
207
|
+
const entries = [];
|
|
208
|
+
const classAttrRegex =
|
|
209
|
+
/<([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?\b(?:class|className)\s*=\s*(["'])([\s\S]*?)\2[^>]*?>/g;
|
|
210
|
+
let match;
|
|
211
|
+
|
|
212
|
+
while ((match = classAttrRegex.exec(content)) !== null) {
|
|
213
|
+
const tagName = match[1].toLowerCase();
|
|
214
|
+
const classes = match[3]
|
|
215
|
+
.split(/\s+/)
|
|
216
|
+
.map((cls) => cls.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
|
|
219
|
+
entries.push({ tagName, classes });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return entries;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasVisibleFocusReplacement(classes) {
|
|
226
|
+
return classes.some((className) => VISIBLE_FOCUS_CLASSES.has(className));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractColourToken(className, prefix) {
|
|
230
|
+
const match = className.match(new RegExp(`^${prefix}([a-z][a-z0-9-]*-\\d{1,3})$`));
|
|
231
|
+
return match ? match[1] : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createAccessibilityWarnings(filePath, content) {
|
|
235
|
+
const warnings = [];
|
|
236
|
+
const classEntries = parseElementClassLists(content);
|
|
237
|
+
|
|
238
|
+
classEntries.forEach(({ tagName, classes }) => {
|
|
239
|
+
if (classes.includes("focus-ring-none") && !hasVisibleFocusReplacement(classes)) {
|
|
240
|
+
warnings.push({
|
|
241
|
+
file: filePath,
|
|
242
|
+
reason: "focus-removal",
|
|
243
|
+
className: "focus-ring-none",
|
|
244
|
+
message:
|
|
245
|
+
'focus-ring-none removes visible focus styles without a replacement focus class.',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const bgTokens = new Set();
|
|
250
|
+
const textTokens = new Set();
|
|
251
|
+
|
|
252
|
+
classes.forEach((className) => {
|
|
253
|
+
const bgToken = extractColourToken(className, "bg-");
|
|
254
|
+
const textToken = extractColourToken(className, "text-");
|
|
255
|
+
|
|
256
|
+
if (bgToken) bgTokens.add(bgToken);
|
|
257
|
+
if (textToken) textTokens.add(textToken);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
bgTokens.forEach((token) => {
|
|
261
|
+
if (textTokens.has(token)) {
|
|
262
|
+
warnings.push({
|
|
263
|
+
file: filePath,
|
|
264
|
+
reason: "same-text-background-colour",
|
|
265
|
+
className: `bg-${token} text-${token}`,
|
|
266
|
+
message: `Text and background both use token "${token}", which is likely unreadable.`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (classes.includes("cursor-pointer") && !INTERACTIVE_TAGS.has(tagName)) {
|
|
272
|
+
warnings.push({
|
|
273
|
+
file: filePath,
|
|
274
|
+
reason: "cursor-pointer-non-interactive",
|
|
275
|
+
className: "cursor-pointer",
|
|
276
|
+
message: `cursor-pointer is applied to non-interactive <${tagName}>.`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return warnings;
|
|
282
|
+
}
|
|
283
|
+
|
|
188
284
|
function doctor() {
|
|
189
285
|
const config = getConfig();
|
|
190
286
|
|
|
@@ -208,6 +304,7 @@ function doctor() {
|
|
|
208
304
|
|
|
209
305
|
const files = getFilesToScan(config);
|
|
210
306
|
const issues = [];
|
|
307
|
+
const warnings = [];
|
|
211
308
|
const suggestionCache = new Map();
|
|
212
309
|
|
|
213
310
|
files.forEach((filePath) => {
|
|
@@ -218,7 +315,8 @@ function doctor() {
|
|
|
218
315
|
classes.forEach((className) => {
|
|
219
316
|
const parsed = normaliseClassForManifest(className);
|
|
220
317
|
const unknownVariants = parsed.variants.filter((variant) => !variantSet.has(variant));
|
|
221
|
-
const knownBase =
|
|
318
|
+
const knownBase =
|
|
319
|
+
utilitySet.has(parsed.baseClass) || KNOWN_CLASS_SHIMS.has(parsed.baseClass);
|
|
222
320
|
|
|
223
321
|
if (unknownVariants.length === 0 && knownBase) {
|
|
224
322
|
return;
|
|
@@ -236,14 +334,31 @@ function doctor() {
|
|
|
236
334
|
suggestion: suggestionCache.get(className),
|
|
237
335
|
});
|
|
238
336
|
});
|
|
337
|
+
|
|
338
|
+
warnings.push(...createAccessibilityWarnings(filePath, content));
|
|
239
339
|
} catch (error) {
|
|
240
340
|
// Keep parity with purge behaviour: unreadable files are skipped.
|
|
241
341
|
}
|
|
242
342
|
});
|
|
243
343
|
|
|
244
|
-
if (issues.length === 0) {
|
|
344
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
245
345
|
console.log("✓ EmilyCSS doctor found no class issues");
|
|
246
|
-
return { ok: true, issues: [], exitCode: 0 };
|
|
346
|
+
return { ok: true, issues: [], warnings: [], exitCode: 0 };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (warnings.length > 0) {
|
|
350
|
+
console.log(
|
|
351
|
+
`EmilyCSS doctor warning${warnings.length === 1 ? "" : "s"} (${warnings.length})\n`,
|
|
352
|
+
);
|
|
353
|
+
warnings.forEach((warning) => {
|
|
354
|
+
console.log(path.relative(process.cwd(), warning.file));
|
|
355
|
+
console.log(` Warning: ${warning.message}`);
|
|
356
|
+
console.log("");
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (issues.length === 0) {
|
|
361
|
+
return { ok: true, issues: [], warnings, exitCode: 0 };
|
|
247
362
|
}
|
|
248
363
|
|
|
249
364
|
console.log(`EmilyCSS doctor found ${issues.length} issue${issues.length === 1 ? "" : "s"}\n`);
|
|
@@ -262,7 +377,7 @@ function doctor() {
|
|
|
262
377
|
});
|
|
263
378
|
|
|
264
379
|
console.log("Run `emily-css build` after fixing classes.");
|
|
265
|
-
return { ok: false, issues, exitCode: 1 };
|
|
380
|
+
return { ok: false, issues, warnings, exitCode: 1 };
|
|
266
381
|
}
|
|
267
382
|
|
|
268
383
|
module.exports = {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function accessibilityUtilities() {
|
|
4
|
+
return `/* Accessibility */
|
|
5
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
6
|
+
.not-sr-only { position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; white-space: normal; }
|
|
7
|
+
.sr-only-focusable:not(:focus):not(:focus-within) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
8
|
+
.focus-ring:focus-visible { outline: 2px solid var(--color-brand-80); outline-offset: 2px; }
|
|
9
|
+
.focus-ring-inset:focus-visible { outline: 2px solid var(--color-brand-80); outline-offset: -2px; }
|
|
10
|
+
.focus-ring-none:focus-visible { outline: none; }
|
|
11
|
+
.focus-visible:focus { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
12
|
+
.focus\\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
|
|
13
|
+
|
|
14
|
+
/* Touch target — WCAG 2.2 SC 2.5.8 minimum 24x24px hit area */
|
|
15
|
+
.touch-target { position: relative; }
|
|
16
|
+
.touch-target::before { content: ''; position: absolute; top: 50%; left: 50%; width: max(100%, 24px); height: max(100%, 24px); transform: translate(-50%, -50%); }
|
|
17
|
+
|
|
18
|
+
/* Skip link — reveals on focus for keyboard/AT users */
|
|
19
|
+
.skip-link { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
20
|
+
.skip-link:focus { position: fixed; top: 1rem; left: 1rem; z-index: 1070; width: auto; height: auto; padding: 0.75rem 1.25rem; background-color: #ffffff; color: #000000; font-weight: 700; text-decoration: underline; border: 2px solid currentColor; border-radius: 4px; clip: auto; white-space: normal; }
|
|
21
|
+
|
|
22
|
+
@media (prefers-reduced-motion: reduce) {
|
|
23
|
+
.motion-reduce\\:transition-none { transition-property: none; }
|
|
24
|
+
.motion-reduce\\:animate-none { animation: none; }
|
|
25
|
+
}
|
|
26
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
27
|
+
.motion-safe\\:transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
|
28
|
+
}
|
|
29
|
+
@media (forced-colors: active) {
|
|
30
|
+
.forced-colors\\:outline { outline: 1px solid CanvasText; }
|
|
31
|
+
.forced-colors\\:outline-1 { outline: 1px solid CanvasText; }
|
|
32
|
+
.forced-colors\\:forced-color-adjust-none { forced-color-adjust: none; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
accessibilityUtilities,
|
|
40
|
+
};
|