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 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.1 — May 2026
7
+ ## v1.2.3 — May 2026
27
8
 
28
- **Refactor utility generators into modules**
9
+ ****
29
10
 
30
11
  ---
31
- ## v1.2.1 — May 2026
32
-
33
- **updated the full system to be more efficient**
12
+ ## v1.2.2 — May 2026
34
13
 
35
14
  ### Added
36
- - updated the full system to be more efficient
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
- - updated release logic
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
- ### Changed
71
- - added new utilties and tests
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
- ### Changed
82
- - updated utilties and new showcase
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
- **A config-driven design system generator for developers working in constrained or legacy environments.**
3
+ Token-first, framework-agnostic CSS generation for teams that want predictable utilities without adopting a full app framework.
4
4
 
5
- Define your brand in one JSON file and generate a production-ready, accessibility-first stylesheet in seconds.
5
+ ## What emilyCSS is
6
6
 
7
- ## The Mental Model
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
- emilyCSS is built for real-world systems like **Drupal, Power Pages, WordPress, static HTML**, and other environments where modern build pipelines often don't exist.
9
+ ## Why teams use it
10
10
 
11
- 1. **Configure** Define your brand colours, fonts, and spacing in `emily.config.json`
12
- 2. **Generate** Run one command to produce a clean, optimized CSS file
13
- 3. **Deploy** Link the stylesheet and copy production-ready components from the showcase
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
- ## Quick Start
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
- This creates your `emily.config.json`, walks you through your brand settings (colours, fonts, spacing, etc.), and runs your first build.
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
- ### 3. Start Building
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 # Setup config + first build
53
- npx emily-css build # Regenerate CSS
54
- npx emily-css watch # Development watch mode
55
- npx emily-css purge # Remove unused styles for production
56
- npx emily-css migrate # Report-only Tailwind-to-EmilyCSS migration analysis
57
- --import-colours # Imported palette mode (visual parity class suggestions)
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
- ## Migration (1.2.0-alpha)
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
- ## How Purge Works
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
- emilyCSS scans your templates for used class names and removes everything else.
53
+ ## Manifest and IntelliSense JSON
70
54
 
71
- Supported files: `.html`, `.php`, `.twig`, `.liquid`, `.jsx`, `.vue`, `.astro`, etc. (configurable).
55
+ Enable machine-readable outputs when needed:
72
56
 
73
- **Important:** Dynamically constructed classes like `bg-${colour}` are not detected. Use static strings or add them to the safelist.
57
+ ```json
58
+ {
59
+ "manifest": true,
60
+ "intellisense": {
61
+ "enabled": true,
62
+ "output": "dist/emily.intellisense.json"
63
+ }
64
+ }
65
+ ```
74
66
 
75
- ## File Size (Typical)
67
+ Generated files:
76
68
 
77
- | State | Size |
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
- ## Configuration
72
+ These files are intended for tooling, audits, and editor integrations. A VSCode extension is not required for generation.
83
73
 
84
- Edit `emily.config.json`:
74
+ ## Minimal configuration example
85
75
 
86
76
  ```json
87
77
  {
88
78
  "name": "My Brand",
89
- "baseUnit": "8px",
90
79
  "fontFamily": {
91
- "heading": "lexend",
80
+ "heading": "atkinson",
92
81
  "body": "inter"
93
82
  },
94
83
  "colours": {
95
- "brand": "#2563EB",
96
- "accent": "#2563EB",
97
- "btn-primary": "#2563EB",
98
- "btn-secondary": "#2563EB",
99
- "neutral": "#57534E"
84
+ "brand": "#0077B6",
85
+ "accent": "#0EA5E9",
86
+ "neutral": "#57534E",
87
+ "success": "#0F766E",
88
+ "error": "#B91C1C"
100
89
  },
101
- "semanticColours": {
102
- "dark": "#1A1A1A",
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
- After changes: `npx emily-css build`
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
- Then import the weights you need.
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
- ## Support
101
+ ## Documentation stubs
152
102
 
153
- - **Website:** https://www.emilyui.com
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({ keepFull: process.argv.includes("--keep-full") });
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.1",
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.0.0"
48
+ "nodemon": "^3.1.14"
49
49
  },
50
50
  "dependencies": {
51
51
  "boxen": "^5.1.2",
52
52
  "chalk": "^4.1.2",
53
- "chokidar": "^5.0.0",
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 = utilitySet.has(parsed.baseClass);
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(manifest, null, 2));
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({ keepFull: args.includes('--keep-full') });
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 isSimpleBaseClassSelector(selector) {
42
- if (!selector || !selector.startsWith('.')) return false;
43
- if (selector.includes(' ')) return false;
44
- if (selector.includes(',')) return false;
45
- if (selector.includes('[')) return false;
46
- if (selector.includes(':')) return false;
47
- if (selector.includes('::')) return false;
48
- if (selector.includes('>')) return false;
49
- if (selector.includes('+')) return false;
50
- if (selector.includes('~')) return false;
51
-
52
- return true;
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
- version: MANIFEST_VERSION,
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 (!isSimpleBaseClassSelector(selector)) continue;
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
- manifest.utilities.push({
276
- class: normalizeClassName(selector),
277
- category: inferCategory(normalizeClassName(selector), firstProperty),
278
- property: firstProperty,
279
- value: firstValue,
280
- token: getTokenFromDeclarations(declarations),
281
- declarations,
282
- variants,
283
- source: 'generated-css',
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
- const classRegex = /(?:class|className)\s*=\s*["']([^"']+)["']/g;
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[1].split(/\s+/);
44
- classes.forEach((cls) => {
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 vueRegex = /(?::class|class\.|v-bind:class)\s*=\s*["'{]([^"'}]+)["'}]/g;
50
- while ((match = vueRegex.exec(content)) !== null) {
51
- const classes = match[1].split(/[\s,]+/);
52
- classes.forEach((cls) => {
53
- const cleaned = cls.replace(/['"`{}"]/g, "").trim();
54
- if (cleaned) classNames.add(cleaned);
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
- cleaned &&
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
 
@@ -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 background-neutral-100">
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">