emily-css 1.2.1 → 1.2.3
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 +129 -71
- package/README.md +60 -111
- package/bin/emilyui.js +6 -1
- package/package.json +3 -3
- package/src/doctor.js +119 -4
- 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,87 +1,145 @@
|
|
|
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.
|
|
7
|
+
## v1.2.3 — May 2026
|
|
27
8
|
|
|
28
|
-
|
|
9
|
+
****
|
|
29
10
|
|
|
30
11
|
---
|
|
31
|
-
## v1.2.
|
|
32
|
-
|
|
33
|
-
**updated the full system to be more efficient**
|
|
12
|
+
## v1.2.2 — May 2026
|
|
34
13
|
|
|
35
14
|
### Added
|
|
36
|
-
-
|
|
15
|
+
- Added IntelliSense JSON generation via `intellisense` config output (`dist/emily.intellisense.json` by default).
|
|
16
|
+
- Added build profiling via `emily-css build --profile` with coarse timing buckets.
|
|
17
|
+
- Added initial accessibility warnings to `emily-css doctor` (focus removal, same token text/background, and `cursor-pointer` on non-interactive elements).
|
|
18
|
+
- Added documentation stubs in `docs/` for installation, configuration, variants, accessibility, doctor, migrate, manifest, and IntelliSense.
|
|
37
19
|
|
|
38
20
|
### Changed
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## v1.1.1 — May 2026
|
|
43
|
-
|
|
44
|
-
**updated changes and added**
|
|
45
|
-
|
|
46
|
-
### Added
|
|
47
|
-
- updted changes
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
## v1.1.0 — May 2026
|
|
51
|
-
|
|
52
|
-
**add utility manifest generation): chore: release v1.1.0**
|
|
53
|
-
|
|
54
|
-
### Added
|
|
55
|
-
- add utility manifest generation): chore: release v1.1.0
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
## v1.0.29 — May 2026
|
|
59
|
-
|
|
60
|
-
**added json manifest for future use**
|
|
61
|
-
|
|
62
|
-
### Added
|
|
63
|
-
- added json manifest for future use
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
## v1.0.28 — May 2026
|
|
67
|
-
|
|
68
|
-
**added new utilities**
|
|
21
|
+
- Stabilised manifest schema metadata with explicit `schemaVersion`, package name, and package version fields.
|
|
22
|
+
- Improved purge class extraction for complex variant patterns and safer junk filtering.
|
|
23
|
+
- Updated README to reflect current product direction and command surface.
|
|
69
24
|
|
|
70
|
-
###
|
|
71
|
-
-
|
|
25
|
+
### Notes
|
|
26
|
+
- EmilyCSS remains CommonJS-compatible and continues to support Node 16+.
|
|
27
|
+
- ESM-only dependency major upgrades remain intentionally deferred for compatibility.
|
|
72
28
|
|
|
73
29
|
---
|
|
74
|
-
## v1.0.27 — May 2026
|
|
75
|
-
|
|
76
|
-
**colour system redesign — brand/accent tokens + semantic colours (v1.0.23)**
|
|
77
|
-
|
|
78
|
-
### Added
|
|
79
|
-
- colour system redesign — brand/accent tokens + semantic colours (v1.0.23)
|
|
80
30
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
31
|
+
## v1.2.1 — May 2026
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Refactored utility generators into smaller internal modules.
|
|
35
|
+
- Kept `src/generators.js` as a compatibility shim so existing imports continue to work.
|
|
36
|
+
- Moved shared defaults into `src/constants.js`.
|
|
37
|
+
- Updated watch mode so configured purge ignore rules are respected consistently.
|
|
38
|
+
- Improved release hardening and package output checks.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Kept dependency versions on CommonJS-compatible majors to support Node 16+.
|
|
42
|
+
- Avoided ESM-only dependency upgrades that would break the current CommonJS CLI.
|
|
43
|
+
|
|
44
|
+
### Dependency compatibility
|
|
45
|
+
EmilyCSS intentionally stays on CommonJS-compatible dependency majors for now:
|
|
46
|
+
|
|
47
|
+
- `chalk@4`
|
|
48
|
+
- `ora@5`
|
|
49
|
+
- `boxen@5`
|
|
50
|
+
- `chokidar@4`
|
|
51
|
+
|
|
52
|
+
Newer major versions are ESM-focused and may require newer Node versions. EmilyCSS currently supports Node 16+ and CommonJS.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## v1.2.0-alpha.0 — May 2026
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Report-only Tailwind-to-EmilyCSS migration command: `emily-css migrate`.
|
|
60
|
+
- Default semantic migration mode for design-token aligned suggestions.
|
|
61
|
+
- Imported palette mode via `emily-css migrate --import-colours` for visual parity mapping suggestions.
|
|
62
|
+
- Detection and reporting for arbitrary value utilities during migration analysis.
|
|
63
|
+
|
|
64
|
+
### Notes
|
|
65
|
+
- Migration in this alpha is analysis-only. No source files are modified by `migrate`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## v1.1.1 — May 2026
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- Utility manifest generation for future tooling, doctor checks, migration support, and editor integrations.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## v1.1.0 — May 2026
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
- Added `emily-css doctor`, a manifest-powered project checker that scans configured source files and reports unknown EmilyCSS classes with suggestions.
|
|
80
|
+
- Added variant-aware class validation for responsive, state, ARIA, data-state, motion, dark, and forced-colours variants.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## v1.2.1 — May 2026
|
|
85
|
+
|
|
86
|
+
**Refactor utility generators into modules**
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
## v1.2.1 — May 2026
|
|
90
|
+
|
|
91
|
+
**updated the full system to be more efficient**
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
- updated the full system to be more efficient
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
- updated release logic
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
## v1.1.1 — May 2026
|
|
101
|
+
|
|
102
|
+
**updated changes and added**
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
- updted changes
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
## v1.1.0 — May 2026
|
|
109
|
+
|
|
110
|
+
**add utility manifest generation): chore: release v1.1.0**
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
- add utility manifest generation): chore: release v1.1.0
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
## v1.0.29 — May 2026
|
|
117
|
+
|
|
118
|
+
**added json manifest for future use**
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- added json manifest for future use
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
## v1.0.28 — May 2026
|
|
125
|
+
|
|
126
|
+
**added new utilities**
|
|
127
|
+
|
|
128
|
+
### Changed
|
|
129
|
+
- added new utilties and tests
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
## v1.0.27 — May 2026
|
|
133
|
+
|
|
134
|
+
**colour system redesign — brand/accent tokens + semantic colours (v1.0.23)**
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
- colour system redesign — brand/accent tokens + semantic colours (v1.0.23)
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
- updated utilties and new showcase
|
|
141
|
+
|
|
142
|
+
---
|
|
85
143
|
# Changelog
|
|
86
144
|
|
|
87
145
|
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.3",
|
|
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 = {
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { generateManifest } = require('./manifest');
|
|
7
|
+
const { generateIntellisense } = require('./intellisense');
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
@@ -1507,6 +1508,32 @@ function getManifestOutputPath(config) {
|
|
|
1507
1508
|
: path.join(process.cwd(), outputPath);
|
|
1508
1509
|
}
|
|
1509
1510
|
|
|
1511
|
+
function getIntellisenseSettings(config) {
|
|
1512
|
+
const intellisenseConfig = config.intellisense;
|
|
1513
|
+
|
|
1514
|
+
if (intellisenseConfig === true) {
|
|
1515
|
+
return { enabled: true, output: 'dist/emily.intellisense.json' };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (intellisenseConfig && typeof intellisenseConfig === 'object') {
|
|
1519
|
+
return {
|
|
1520
|
+
enabled: intellisenseConfig.enabled === true,
|
|
1521
|
+
output: intellisenseConfig.output || 'dist/emily.intellisense.json',
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return { enabled: false, output: 'dist/emily.intellisense.json' };
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function getIntellisenseOutputPath(config) {
|
|
1529
|
+
const intellisenseSettings = getIntellisenseSettings(config);
|
|
1530
|
+
const outputPath = intellisenseSettings.output || 'dist/emily.intellisense.json';
|
|
1531
|
+
|
|
1532
|
+
return path.isAbsolute(outputPath)
|
|
1533
|
+
? outputPath
|
|
1534
|
+
: path.join(process.cwd(), outputPath);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1510
1537
|
function ensureDirectoryForFile(filePath) {
|
|
1511
1538
|
const dir = path.dirname(filePath);
|
|
1512
1539
|
|
|
@@ -1729,15 +1756,30 @@ ${bodyFont}`;
|
|
|
1729
1756
|
fs.writeFileSync(fullCssPath, css);
|
|
1730
1757
|
|
|
1731
1758
|
const manifestSettings = getManifestSettings(config);
|
|
1759
|
+
const intellisenseSettings = getIntellisenseSettings(config);
|
|
1760
|
+
const shouldGenerateManifestData =
|
|
1761
|
+
manifestSettings.enabled || intellisenseSettings.enabled;
|
|
1762
|
+
const manifestData = shouldGenerateManifestData
|
|
1763
|
+
? generateManifest(css, config)
|
|
1764
|
+
: null;
|
|
1765
|
+
|
|
1732
1766
|
if (manifestSettings.enabled) {
|
|
1733
1767
|
const manifestPath = getManifestOutputPath(config);
|
|
1734
|
-
const manifest = generateManifest(css, config);
|
|
1735
1768
|
|
|
1736
1769
|
ensureDirectoryForFile(manifestPath);
|
|
1737
|
-
fs.writeFileSync(manifestPath, JSON.stringify(
|
|
1770
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
|
|
1738
1771
|
console.log(`✓ Generated manifest: ${manifestPath}`);
|
|
1739
1772
|
}
|
|
1740
1773
|
|
|
1774
|
+
if (intellisenseSettings.enabled) {
|
|
1775
|
+
const intellisensePath = getIntellisenseOutputPath(config);
|
|
1776
|
+
const intellisense = generateIntellisense(manifestData);
|
|
1777
|
+
|
|
1778
|
+
ensureDirectoryForFile(intellisensePath);
|
|
1779
|
+
fs.writeFileSync(intellisensePath, JSON.stringify(intellisense, null, 2));
|
|
1780
|
+
console.log(`✓ Generated IntelliSense: ${intellisensePath}`);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1741
1783
|
console.log(`✓ Generated CSS: ${fullCssPath}`);
|
|
1742
1784
|
console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
|
|
1743
1785
|
console.log('Full framework build complete');
|
|
@@ -1753,11 +1795,16 @@ function minify(css) {
|
|
|
1753
1795
|
.trim();
|
|
1754
1796
|
}
|
|
1755
1797
|
|
|
1756
|
-
function buildProductionCss() {
|
|
1798
|
+
function buildProductionCss(options = {}) {
|
|
1757
1799
|
const config = getConfig();
|
|
1758
1800
|
const sourceDir = getSourceDir(config);
|
|
1759
1801
|
const fullCssPath = getFullCssPath(config);
|
|
1760
1802
|
const productionCssPath = getProductionCssPath(config);
|
|
1803
|
+
const profile = {
|
|
1804
|
+
purge: 0,
|
|
1805
|
+
minify: 0,
|
|
1806
|
+
write: 0,
|
|
1807
|
+
};
|
|
1761
1808
|
|
|
1762
1809
|
if (!fs.existsSync(fullCssPath)) {
|
|
1763
1810
|
buildFullFramework();
|
|
@@ -1765,11 +1812,18 @@ function buildProductionCss() {
|
|
|
1765
1812
|
|
|
1766
1813
|
const { purgeCSS } = require('./purge.js');
|
|
1767
1814
|
const css = fs.readFileSync(fullCssPath, 'utf8');
|
|
1815
|
+
const purgeStart = Date.now();
|
|
1768
1816
|
const purged = purgeCSS(css, sourceDir, config);
|
|
1817
|
+
profile.purge = Date.now() - purgeStart;
|
|
1818
|
+
|
|
1819
|
+
const minifyStart = Date.now();
|
|
1769
1820
|
const minified = minify(purged);
|
|
1821
|
+
profile.minify = Date.now() - minifyStart;
|
|
1770
1822
|
|
|
1823
|
+
const writeStart = Date.now();
|
|
1771
1824
|
ensureDirectoryForFile(productionCssPath);
|
|
1772
1825
|
fs.writeFileSync(productionCssPath, minified);
|
|
1826
|
+
profile.write = Date.now() - writeStart;
|
|
1773
1827
|
|
|
1774
1828
|
return {
|
|
1775
1829
|
css,
|
|
@@ -1779,6 +1833,7 @@ function buildProductionCss() {
|
|
|
1779
1833
|
outputSize: Buffer.byteLength(minified, 'utf8'),
|
|
1780
1834
|
outputPath: productionCssPath,
|
|
1781
1835
|
fullCssPath,
|
|
1836
|
+
profile: options.profile ? profile : undefined,
|
|
1782
1837
|
};
|
|
1783
1838
|
}
|
|
1784
1839
|
|
|
@@ -1800,11 +1855,14 @@ function ensureFullFramework() {
|
|
|
1800
1855
|
}
|
|
1801
1856
|
|
|
1802
1857
|
function build(options = {}) {
|
|
1858
|
+
const totalStart = Date.now();
|
|
1859
|
+
const frameworkStart = Date.now();
|
|
1803
1860
|
ensureFullFramework();
|
|
1861
|
+
const frameworkMs = Date.now() - frameworkStart;
|
|
1804
1862
|
|
|
1805
1863
|
const config = getConfig();
|
|
1806
1864
|
const fullCssPath = getFullCssPath(config);
|
|
1807
|
-
const result = buildProductionCss();
|
|
1865
|
+
const result = buildProductionCss({ profile: options.profile });
|
|
1808
1866
|
|
|
1809
1867
|
console.log('\u2713 Generated production CSS: ' + path.relative(process.cwd(), result.outputPath));
|
|
1810
1868
|
console.log('\u2713 File size: ' + (result.outputSize / 1024).toFixed(2) + ' KB');
|
|
@@ -1818,12 +1876,27 @@ function build(options = {}) {
|
|
|
1818
1876
|
}
|
|
1819
1877
|
}
|
|
1820
1878
|
|
|
1879
|
+
if (options.profile) {
|
|
1880
|
+
const timings = result.profile || { purge: 0, minify: 0, write: 0 };
|
|
1881
|
+
const totalMs = Date.now() - totalStart;
|
|
1882
|
+
|
|
1883
|
+
console.log('\nEmilyCSS build profile\n');
|
|
1884
|
+
console.log('Framework: ' + frameworkMs + 'ms');
|
|
1885
|
+
console.log('Purge: ' + timings.purge + 'ms');
|
|
1886
|
+
console.log('Minify: ' + timings.minify + 'ms');
|
|
1887
|
+
console.log('Write: ' + timings.write + 'ms');
|
|
1888
|
+
console.log('Total: ' + totalMs + 'ms');
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1821
1891
|
console.log('Build complete');
|
|
1822
1892
|
}
|
|
1823
1893
|
|
|
1824
1894
|
if (require.main === module) {
|
|
1825
1895
|
const args = process.argv.slice(2);
|
|
1826
|
-
build({
|
|
1896
|
+
build({
|
|
1897
|
+
keepFull: args.includes('--keep-full'),
|
|
1898
|
+
profile: args.includes('--profile'),
|
|
1899
|
+
});
|
|
1827
1900
|
}
|
|
1828
1901
|
|
|
1829
1902
|
module.exports = {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function toIntellisenseUtility(utility) {
|
|
2
|
+
return {
|
|
3
|
+
class: utility.class || null,
|
|
4
|
+
category: utility.category || null,
|
|
5
|
+
property: utility.property || null,
|
|
6
|
+
value: utility.value || null,
|
|
7
|
+
token: utility.token || null,
|
|
8
|
+
variants: Array.isArray(utility.variants) ? utility.variants : [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateIntellisense(manifest) {
|
|
13
|
+
const utilities = Array.isArray(manifest && manifest.utilities)
|
|
14
|
+
? manifest.utilities
|
|
15
|
+
: [];
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
version: "1",
|
|
19
|
+
generatedAt:
|
|
20
|
+
(manifest && manifest.generatedAt) || new Date().toISOString(),
|
|
21
|
+
utilities: utilities.map(toIntellisenseUtility),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
generateIntellisense,
|
|
27
|
+
};
|
package/src/manifest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MANIFEST_VERSION = '1.1.0';
|
|
2
|
+
const packageJson = require('../package.json');
|
|
2
3
|
const {
|
|
3
4
|
DEFAULT_RESPONSIVE_VARIANTS,
|
|
4
5
|
BASE_VARIANTS,
|
|
@@ -38,18 +39,76 @@ function getTokenFromDeclarations(declarations) {
|
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function
|
|
42
|
-
if (!selector || !selector.startsWith('.')) return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
function readLeadingClassSelector(selector) {
|
|
43
|
+
if (!selector || !selector.startsWith('.')) return null;
|
|
44
|
+
|
|
45
|
+
let classSelector = '.';
|
|
46
|
+
|
|
47
|
+
for (let i = 1; i < selector.length; i++) {
|
|
48
|
+
const char = selector[i];
|
|
49
|
+
|
|
50
|
+
if (char === '\\' && i + 1 < selector.length) {
|
|
51
|
+
classSelector += selector[i] + selector[i + 1];
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
char === ' ' ||
|
|
58
|
+
char === '\t' ||
|
|
59
|
+
char === '\n' ||
|
|
60
|
+
char === '\r' ||
|
|
61
|
+
char === ',' ||
|
|
62
|
+
char === '>' ||
|
|
63
|
+
char === '+' ||
|
|
64
|
+
char === '~' ||
|
|
65
|
+
char === ':' ||
|
|
66
|
+
char === '['
|
|
67
|
+
) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
classSelector += char;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return classSelector.length > 1 ? classSelector : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractManifestClassSelectors(selector) {
|
|
78
|
+
if (typeof selector !== 'string' || selector.trim().length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const classSelectors = [];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const selectorParts = selector
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((part) => part.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
|
|
89
|
+
selectorParts.forEach((part) => {
|
|
90
|
+
const classSelector = readLeadingClassSelector(part);
|
|
91
|
+
if (!classSelector) return;
|
|
92
|
+
if (classSelector.includes('\\:')) return;
|
|
93
|
+
|
|
94
|
+
const remainder = part.slice(classSelector.length).trimStart();
|
|
95
|
+
|
|
96
|
+
// Expanded pseudo-state / pseudo-element and attribute selectors are not
|
|
97
|
+
// base utilities in the manifest (for example .hover\:x:hover, .x[...]).
|
|
98
|
+
if (
|
|
99
|
+
remainder.startsWith(':') ||
|
|
100
|
+
remainder.startsWith('::') ||
|
|
101
|
+
remainder.startsWith('[')
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (seen.has(classSelector)) return;
|
|
107
|
+
seen.add(classSelector);
|
|
108
|
+
classSelectors.push(classSelector);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return classSelectors;
|
|
53
112
|
}
|
|
54
113
|
|
|
55
114
|
function inferCategory(className, property) {
|
|
@@ -249,7 +308,10 @@ function getVariants(config) {
|
|
|
249
308
|
|
|
250
309
|
function generateManifest(css, config = {}) {
|
|
251
310
|
const manifest = {
|
|
252
|
-
|
|
311
|
+
schemaVersion: '1',
|
|
312
|
+
package: packageJson.name || 'emily-css',
|
|
313
|
+
version: packageJson.version || '0.0.0',
|
|
314
|
+
manifestVersion: MANIFEST_VERSION,
|
|
253
315
|
generatedAt: new Date().toISOString(),
|
|
254
316
|
utilities: [],
|
|
255
317
|
};
|
|
@@ -266,21 +328,25 @@ function generateManifest(css, config = {}) {
|
|
|
266
328
|
while ((ruleMatch = ruleRegex.exec(cleanedCss)) !== null) {
|
|
267
329
|
const selector = ruleMatch[1].trim();
|
|
268
330
|
const declarationBlock = ruleMatch[2].trim();
|
|
269
|
-
|
|
270
|
-
if (
|
|
331
|
+
const classSelectors = extractManifestClassSelectors(selector);
|
|
332
|
+
if (classSelectors.length === 0) continue;
|
|
271
333
|
|
|
272
334
|
const { declarations, firstProperty, firstValue } = parseDeclarations(declarationBlock);
|
|
273
335
|
if (!firstProperty) continue;
|
|
274
336
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
337
|
+
classSelectors.forEach((classSelector) => {
|
|
338
|
+
const className = normalizeClassName(classSelector);
|
|
339
|
+
|
|
340
|
+
manifest.utilities.push({
|
|
341
|
+
class: className,
|
|
342
|
+
category: inferCategory(className, firstProperty),
|
|
343
|
+
property: firstProperty,
|
|
344
|
+
value: firstValue,
|
|
345
|
+
token: getTokenFromDeclarations(declarations),
|
|
346
|
+
declarations,
|
|
347
|
+
variants,
|
|
348
|
+
source: 'generated-css',
|
|
349
|
+
});
|
|
284
350
|
});
|
|
285
351
|
}
|
|
286
352
|
|
package/src/purge.js
CHANGED
|
@@ -36,23 +36,48 @@ function getAllFiles(dir, extensions = DEFAULT_EXTENSIONS) {
|
|
|
36
36
|
|
|
37
37
|
function extractClassNames(content) {
|
|
38
38
|
const classNames = new Set();
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
function isLikelyClassToken(token) {
|
|
41
|
+
if (!token || typeof token !== "string") return false;
|
|
42
|
+
if (token.length > 120) return false;
|
|
43
|
+
if (token.includes("://")) return false;
|
|
44
|
+
if (token.startsWith(".") || token.startsWith("#") || token.startsWith("@")) return false;
|
|
45
|
+
if (token.endsWith(":")) return false;
|
|
46
|
+
if (/[(){};,`$]/.test(token)) return false;
|
|
47
|
+
if (!/^[a-zA-Z0-9:#_./\-[\]]+$/.test(token)) return false;
|
|
48
|
+
if (!/[a-zA-Z]/.test(token)) return false;
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addClassToken(token) {
|
|
54
|
+
if (!token) return;
|
|
55
|
+
|
|
56
|
+
const cleaned = token.trim().replace(/^['"`]+|['"`]+$/g, "");
|
|
57
|
+
if (!cleaned) return;
|
|
58
|
+
if (!isLikelyClassToken(cleaned)) return;
|
|
59
|
+
|
|
60
|
+
classNames.add(cleaned);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const classRegex = /(?:class|className)\s*=\s*(["'])([\s\S]*?)\1/g;
|
|
40
64
|
let match;
|
|
41
65
|
|
|
42
66
|
while ((match = classRegex.exec(content)) !== null) {
|
|
43
|
-
const classes = match[
|
|
44
|
-
classes.forEach(
|
|
45
|
-
if (cls.trim()) classNames.add(cls.trim());
|
|
46
|
-
});
|
|
67
|
+
const classes = match[2].split(/\s+/);
|
|
68
|
+
classes.forEach(addClassToken);
|
|
47
69
|
}
|
|
48
70
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
const vueBindingRegex = /(?:\:class|v-bind:class)\s*=\s*(["'])([\s\S]*?)\1/g;
|
|
72
|
+
const vueObjectKeyRegex = /['"`]([^'"`]+)['"`]\s*:/g;
|
|
73
|
+
|
|
74
|
+
while ((match = vueBindingRegex.exec(content)) !== null) {
|
|
75
|
+
const bindingContent = match[2];
|
|
76
|
+
let keyMatch;
|
|
77
|
+
|
|
78
|
+
while ((keyMatch = vueObjectKeyRegex.exec(bindingContent)) !== null) {
|
|
79
|
+
addClassToken(keyMatch[1]);
|
|
80
|
+
}
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
const templateStringRegex = /`([^`]+)`/g;
|
|
@@ -62,14 +87,9 @@ function extractClassNames(content) {
|
|
|
62
87
|
|
|
63
88
|
possibleClasses.forEach((cls) => {
|
|
64
89
|
const cleaned = cls.trim();
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
/^[a-zA-Z0-9:_./-]+$/.test(cleaned) &&
|
|
69
|
-
/[-:]/.test(cleaned)
|
|
70
|
-
) {
|
|
71
|
-
classNames.add(cleaned);
|
|
72
|
-
}
|
|
90
|
+
if (!cleaned) return;
|
|
91
|
+
if (!/[-:]/.test(cleaned)) return;
|
|
92
|
+
addClassToken(cleaned);
|
|
73
93
|
});
|
|
74
94
|
}
|
|
75
95
|
|
package/templates/showcase.html
CHANGED
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
</div>
|
|
150
150
|
</section>
|
|
151
151
|
|
|
152
|
-
<section id="buttons" class="bg-white rounded-lg shadow-md border border-neutral-20 p-6 mb-6
|
|
152
|
+
<section id="buttons" class="bg-white rounded-lg shadow-md border border-neutral-20 p-6 mb-6">
|
|
153
153
|
<h2 class="text-2xl font-bold mb-4">Buttons</h2>
|
|
154
154
|
|
|
155
155
|
<div class="flex flex-wrap gap-3">
|