emily-css 1.2.6 → 1.2.8

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
@@ -4,6 +4,50 @@ All notable changes to `emily-css` are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v1.2.8 — May 2026
8
+
9
+ **feat(purge): add purge safelist support and keep semantic dark/light utilities**
10
+
11
+ ### Added
12
+ - feat(purge): add purge safelist support and keep semantic dark/light utilities
13
+ - add purge safelist support and keep semantic dark/light utilities
14
+ - widen utility-prefix detection for scanner
15
+ - expand Tailwind-compatible utility coverage
16
+
17
+ ### Changed
18
+ - add coverage for new utility classes
19
+
20
+ ---
21
+ ## v1.2.8 — May 2026
22
+
23
+ **Expand Tailwind-compat utility coverage and align migration scanner utility-family detection.**
24
+
25
+ ### Added
26
+ - utilities: negative margin set (`-m-*`, `-mx-*`, `-my-*`, `-mt-*`, `-mr-*`, `-mb-*`, `-ml-*`)
27
+ - utilities: full positioning values (`top/right/bottom/left/inset(-x/-y)-full`)
28
+ - utilities: `box-border`, `box-content`
29
+ - utilities: `justify-items-*`, `justify-self-*`
30
+ - utilities: `max-h-none`
31
+ - utilities: font weight completion (`font-thin`, `font-extralight`, `font-extrabold`, `font-black`)
32
+ - utilities: `bg-origin-*` and `bg-gradient-to-*`
33
+ - utilities: `transition-all`, `transition-shadow`
34
+ - utilities: `scale-x-*`, `scale-y-*`, expanded skew and negative skew variants
35
+ - utilities: full overscroll set (`overscroll-*`, `overscroll-x-*`, `overscroll-y-*`)
36
+
37
+ ### Changed
38
+ - migrate: expanded utility-prefix detection to include `box-*`, `overscroll-*`, `transition-*`, `color-scheme-*`, `field-sizing-*`, and `scrollbar-*` families.
39
+ - docs: updated README and migrate documentation with new compatibility coverage.
40
+
41
+ ---
42
+
43
+ ## v1.2.7 — May 2026
44
+
45
+ **Fix migration scanner filtering so Vue dynamic class placeholders such as [rootClass] are not reported as unsupported arbitrary value utilities.**
46
+
47
+ ### Fixed
48
+ - fix: ignore dynamic class placeholders in migration scan
49
+
50
+ ---
7
51
  ## v1.2.6 — May 2026
8
52
 
9
53
  **Fix migration scanner false positives by ignoring prose tokens, JS property access, and dynamic placeholders while keeping valid utility classes and arbitrary value support.**
@@ -455,4 +499,4 @@ All notable changes to `emily-css` are documented here.
455
499
  - 11,844 utility classes generated from `emily.config.json`
456
500
  - OKLCH colour scale generation — one hex in, 10-shade scale out
457
501
  - Responsive variants (`sm:` `md:` `lg:` `xl:` `2xl:`)
458
- - State variants (`hover:` `focus-visible:` `active:` `disab
502
+ - State variants (`hover:` `focus-visible:` `active:` `disab
package/README.md CHANGED
@@ -11,6 +11,7 @@ emilyCSS lets you define design tokens in `emily.config.json` and generate stati
11
11
  - Token-first utility generation from your own colours, spacing, typography, and motion settings
12
12
  - Framework-agnostic output (`dist/emily.css` and `dist/emily.min.css`)
13
13
  - Accessibility-focused utility coverage (focus rings, visually-hidden helpers, motion-aware variants)
14
+ - Broader Tailwind-style compatibility coverage for everyday migration classes
14
15
  - Tooling support with manifest and IntelliSense JSON generation
15
16
  - CommonJS package with Node 18+ compatibility
16
17
 
@@ -67,6 +68,23 @@ npm run emily:help
67
68
  - `migrate` is report-only and helps plan Tailwind-to-Emily migrations without modifying files.
68
69
  - For best migrate accuracy, generate the full framework/manifest first (`emily-css build --keep-full` or enable `manifest: true`).
69
70
 
71
+ ## Tailwind compatibility additions
72
+
73
+ Recent utility coverage additions include:
74
+
75
+ - Negative margin utilities: `-m-*`, `-mx-*`, `-my-*`, `-mt-*`, `-mr-*`, `-mb-*`, `-ml-*`
76
+ - Positioning full values: `top-full`, `right-full`, `bottom-full`, `left-full`, `inset-full`, `inset-x-full`, `inset-y-full`
77
+ - Box sizing: `box-border`, `box-content`
78
+ - Grid alignment: `justify-items-*`, `justify-self-*`
79
+ - Sizing completion: `max-h-none`
80
+ - Typography completion: `font-thin`, `font-extralight`, `font-extrabold`, `font-black`
81
+ - Background origin and gradient directions: `bg-origin-*`, `bg-gradient-to-*`
82
+ - Transition completion: `transition-all`, `transition-shadow`
83
+ - Transform axis utilities: `scale-x-*`, `scale-y-*`, extended `skew-x-*`, `skew-y-*`, and negative skew variants
84
+ - Overscroll behavior: `overscroll-*`, `overscroll-x-*`, `overscroll-y-*`
85
+
86
+ Migration scanner utility-prefix detection was also expanded for classes like `box-*`, `overscroll-*`, `transition-*`, `color-scheme-*`, `field-sizing-*`, and `scrollbar-*`.
87
+
70
88
  ## Manifest and IntelliSense JSON
71
89
 
72
90
  Enable machine-readable outputs when needed:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emily-css",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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": {
@@ -9,6 +9,9 @@ function backgroundUtilities() {
9
9
  .bg-clip-padding { background-clip: padding-box; }
10
10
  .bg-clip-content { background-clip: content-box; }
11
11
  .bg-clip-text { -webkit-background-clip: text; background-clip: text; }
12
+ .bg-origin-border { background-origin: border-box; }
13
+ .bg-origin-padding { background-origin: padding-box; }
14
+ .bg-origin-content { background-origin: content-box; }
12
15
  .bg-repeat { background-repeat: repeat; }
13
16
  .bg-no-repeat { background-repeat: no-repeat; }
14
17
  .bg-repeat-x { background-repeat: repeat-x; }
@@ -27,6 +30,14 @@ function backgroundUtilities() {
27
30
  .bg-left-bottom { background-position: left bottom; }
28
31
  .bg-right-top { background-position: right top; }
29
32
  .bg-right-bottom { background-position: right bottom; }
33
+ .bg-gradient-to-t { background-image: linear-gradient(to top, var(--tw-gradient-stops)); }
34
+ .bg-gradient-to-tr { background-image: linear-gradient(to top right, var(--tw-gradient-stops)); }
35
+ .bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
36
+ .bg-gradient-to-br { background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); }
37
+ .bg-gradient-to-b { background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); }
38
+ .bg-gradient-to-bl { background-image: linear-gradient(to bottom left, var(--tw-gradient-stops)); }
39
+ .bg-gradient-to-l { background-image: linear-gradient(to left, var(--tw-gradient-stops)); }
40
+ .bg-gradient-to-tl { background-image: linear-gradient(to top left, var(--tw-gradient-stops)); }
30
41
 
31
42
  `;
32
43
  }
@@ -37,6 +37,8 @@ function displayUtilities() {
37
37
  .clear-right { clear: right; }
38
38
  .clear-both { clear: both; }
39
39
  .clear-none { clear: none; }
40
+ .box-border { box-sizing: border-box; }
41
+ .box-content { box-sizing: content-box; }
40
42
 
41
43
  `;
42
44
  }
@@ -16,8 +16,10 @@ function transitionUtilities() {
16
16
  return `/* Transitions */
17
17
  .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; }
18
18
  .transition-none { transition-property: none; }
19
+ .transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
19
20
  .transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
20
21
  .transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
22
+ .transition-shadow { transition-property: box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
21
23
  .transition-transform { transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
22
24
  .duration-75 { transition-duration: 75ms; }
23
25
  .duration-100 { transition-duration: 100ms; }
@@ -17,6 +17,15 @@ function overflowUtilities() {
17
17
  .overflow-y-clip { overflow-y: clip; }
18
18
  .overflow-y-visible { overflow-y: visible; }
19
19
  .overflow-y-scroll { overflow-y: scroll; }
20
+ .overscroll-auto { overscroll-behavior: auto; }
21
+ .overscroll-contain { overscroll-behavior: contain; }
22
+ .overscroll-none { overscroll-behavior: none; }
23
+ .overscroll-x-auto { overscroll-behavior-x: auto; }
24
+ .overscroll-x-contain { overscroll-behavior-x: contain; }
25
+ .overscroll-x-none { overscroll-behavior-x: none; }
26
+ .overscroll-y-auto { overscroll-behavior-y: auto; }
27
+ .overscroll-y-contain { overscroll-behavior-y: contain; }
28
+ .overscroll-y-none { overscroll-behavior-y: none; }
20
29
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
30
  .text-ellipsis { text-overflow: ellipsis; }
22
31
  .text-clip { text-overflow: clip; }
@@ -36,6 +36,20 @@ function positioningUtilities(spacing) {
36
36
  css += `.inset-auto { inset: auto; }\n`;
37
37
  css += `.inset-x-auto { left: auto; right: auto; }\n`;
38
38
  css += `.inset-y-auto { top: auto; bottom: auto; }\n`;
39
+ css += `.inset-full { inset: 100%; }\n`;
40
+ css += `.inset-x-full { left: 100%; right: 100%; }\n`;
41
+ css += `.inset-y-full { top: 100%; bottom: 100%; }\n`;
42
+ css += `.top-full { top: 100%; }\n`;
43
+ css += `.right-full { right: 100%; }\n`;
44
+ css += `.bottom-full { bottom: 100%; }\n`;
45
+ css += `.left-full { left: 100%; }\n`;
46
+ css += `.-inset-full { inset: -100%; }\n`;
47
+ css += `.-inset-x-full { left: -100%; right: -100%; }\n`;
48
+ css += `.-inset-y-full { top: -100%; bottom: -100%; }\n`;
49
+ css += `.-top-full { top: -100%; }\n`;
50
+ css += `.-right-full { right: -100%; }\n`;
51
+ css += `.-bottom-full { bottom: -100%; }\n`;
52
+ css += `.-left-full { left: -100%; }\n`;
39
53
  css += `.top-auto { top: auto; }\n`;
40
54
  css += `.right-auto { right: auto; }\n`;
41
55
  css += `.bottom-auto { bottom: auto; }\n`;
@@ -75,6 +75,7 @@ function sizingUtilities(spacing) {
75
75
  css += `.max-w-max { max-width: max-content; }\n`;
76
76
  css += `.max-w-fit { max-width: fit-content; }\n`;
77
77
  css += `.max-h-0 { max-height: 0; }\n`;
78
+ css += `.max-h-none { max-height: none; }\n`;
78
79
  css += `.max-h-full { max-height: 100%; }\n`;
79
80
  css += `.max-h-screen { max-height: 100vh; }\n`;
80
81
  css += `.max-h-svh { max-height: 100svh; }\n`;
@@ -32,13 +32,19 @@ function transformUtilities(spacing) {
32
32
  const scales = [0, 50, 75, 90, 95, 100, 110, 125, 150];
33
33
  scales.forEach(scale => {
34
34
  css += `.scale-${scale} { --scale-x: ${scale / 100}; --scale-y: ${scale / 100}; transform: ${composedTransform}; }\n`;
35
+ css += `.scale-x-${scale} { --scale-x: ${scale / 100}; transform: ${composedTransform}; }\n`;
36
+ css += `.scale-y-${scale} { --scale-y: ${scale / 100}; transform: ${composedTransform}; }\n`;
35
37
  });
36
38
 
37
39
  // Skew
38
- const skews = [0, 1, 2, 3];
40
+ const skews = [0, 1, 2, 3, 6, 12];
39
41
  skews.forEach(sk => {
40
42
  css += `.skew-x-${sk} { --skew-x: ${sk}deg; transform: ${composedTransform}; }\n`;
41
43
  css += `.skew-y-${sk} { --skew-y: ${sk}deg; transform: ${composedTransform}; }\n`;
44
+ if (sk > 0) {
45
+ css += `.-skew-x-${sk} { --skew-x: -${sk}deg; transform: ${composedTransform}; }\n`;
46
+ css += `.-skew-y-${sk} { --skew-y: -${sk}deg; transform: ${composedTransform}; }\n`;
47
+ }
42
48
  });
43
49
 
44
50
  // Transform origin
package/src/index.js CHANGED
@@ -356,6 +356,17 @@ function generateSpacingUtilities(spacing) {
356
356
  css += `.ml-${escaped} { margin-left: ${value}; }\n`;
357
357
  css += `.ms-${escaped} { margin-inline-start: ${value}; }\n`;
358
358
  css += `.me-${escaped} { margin-inline-end: ${value}; }\n`;
359
+ if (value !== '0' && value !== '0px') {
360
+ css += `.-m-${escaped} { margin: -${value}; }\n`;
361
+ css += `.-mx-${escaped} { margin-left: -${value}; margin-right: -${value}; }\n`;
362
+ css += `.-my-${escaped} { margin-top: -${value}; margin-bottom: -${value}; }\n`;
363
+ css += `.-mt-${escaped} { margin-top: -${value}; }\n`;
364
+ css += `.-mr-${escaped} { margin-right: -${value}; }\n`;
365
+ css += `.-mb-${escaped} { margin-bottom: -${value}; }\n`;
366
+ css += `.-ml-${escaped} { margin-left: -${value}; }\n`;
367
+ css += `.-ms-${escaped} { margin-inline-start: -${value}; }\n`;
368
+ css += `.-me-${escaped} { margin-inline-end: -${value}; }\n`;
369
+ }
359
370
  });
360
371
 
361
372
  // Margin auto
@@ -552,6 +563,16 @@ function generateGridUtilities(spacing) {
552
563
  css += `.gap-y-${escaped} { row-gap: ${value}; }\n`;
553
564
  });
554
565
 
566
+ css += `.justify-items-start { justify-items: start; }\n`;
567
+ css += `.justify-items-end { justify-items: end; }\n`;
568
+ css += `.justify-items-center { justify-items: center; }\n`;
569
+ css += `.justify-items-stretch { justify-items: stretch; }\n`;
570
+ css += `.justify-self-auto { justify-self: auto; }\n`;
571
+ css += `.justify-self-start { justify-self: start; }\n`;
572
+ css += `.justify-self-end { justify-self: end; }\n`;
573
+ css += `.justify-self-center { justify-self: center; }\n`;
574
+ css += `.justify-self-stretch { justify-self: stretch; }\n`;
575
+
555
576
  css += `\n`;
556
577
  return css;
557
578
  }
@@ -567,7 +588,23 @@ function generateTypographyUtilities(config) {
567
588
  css += `.text-${fontSize.name} { font-size: var(--text-${fontSize.name}); line-height: ${fontSize.lineHeight}; }\n`;
568
589
  });
569
590
 
570
- Object.entries(config.typography.fontWeights).forEach(([name, weight]) => {
591
+ const fontWeightDefaults = {
592
+ thin: 100,
593
+ extralight: 200,
594
+ light: 300,
595
+ normal: 400,
596
+ medium: 500,
597
+ semibold: 600,
598
+ bold: 700,
599
+ extrabold: 800,
600
+ black: 900,
601
+ };
602
+ const resolvedFontWeights = {
603
+ ...fontWeightDefaults,
604
+ ...(config.typography.fontWeights || {}),
605
+ };
606
+
607
+ Object.entries(resolvedFontWeights).forEach(([name, weight]) => {
571
608
  css += `.font-${name} { font-weight: ${weight}; }\n`;
572
609
  });
573
610
 
package/src/init.js CHANGED
@@ -56,6 +56,16 @@ const FONT_OPTIONS = [
56
56
  { name: "atkinson", message: "Atkinson Hyperlegible (maximum legibility)" },
57
57
  { name: "system", message: "System sans-serif (no download required)" },
58
58
  ];
59
+ const CORE_COLOUR_KEYS = new Set([
60
+ "brand",
61
+ "accent",
62
+ "btn-primary",
63
+ "btn-secondary",
64
+ "success",
65
+ "warning",
66
+ "error",
67
+ "neutral",
68
+ ]);
59
69
 
60
70
  // ============================================================================
61
71
  // HELPERS
@@ -65,10 +75,47 @@ function isValidHex(hex) {
65
75
  return /^#[0-9A-F]{6}$/i.test(hex);
66
76
  }
67
77
 
78
+ function isPlainObject(value) {
79
+ return (
80
+ value !== null &&
81
+ typeof value === "object" &&
82
+ !Array.isArray(value)
83
+ );
84
+ }
85
+
86
+ function mergeWithDefaults(defaults, existing) {
87
+ if (!isPlainObject(defaults)) {
88
+ return existing === undefined ? defaults : existing;
89
+ }
90
+
91
+ const output = { ...defaults };
92
+
93
+ if (!isPlainObject(existing)) {
94
+ return output;
95
+ }
96
+
97
+ Object.keys(existing).forEach((key) => {
98
+ if (isPlainObject(defaults[key]) && isPlainObject(existing[key])) {
99
+ output[key] = mergeWithDefaults(defaults[key], existing[key]);
100
+ return;
101
+ }
102
+
103
+ output[key] = existing[key];
104
+ });
105
+
106
+ return output;
107
+ }
108
+
68
109
  function colourSwatch(hex) {
69
110
  return chalk.hex(hex)("■");
70
111
  }
71
112
 
113
+ function normaliseHex(value) {
114
+ return typeof value === "string" && isValidHex(value)
115
+ ? value.toUpperCase()
116
+ : null;
117
+ }
118
+
72
119
  async function askHex(promptName, message, initial) {
73
120
  const value = await new Input({
74
121
  name: promptName,
@@ -84,28 +131,69 @@ async function askHex(promptName, message, initial) {
84
131
  return value.toUpperCase();
85
132
  }
86
133
 
87
- async function askColourFromPresets(label, presets, defaultHex) {
134
+ async function askColourFromPresets(label, presets, defaultHex, currentHex) {
135
+ const defaultHexValue = normaliseHex(defaultHex);
136
+ const currentHexValue = normaliseHex(currentHex);
137
+
88
138
  const choices = presets.map(function (opt) {
89
139
  if (opt.value === "custom") {
90
140
  return { name: "custom", message: "Enter your own hex" };
91
141
  }
92
142
 
143
+ const upperHex = String(opt.value).toUpperCase();
93
144
  return {
94
- name: opt.value,
145
+ name: upperHex,
95
146
  message:
96
- colourSwatch(opt.value) + " " + opt.label + " " + chalk.gray(opt.value),
147
+ colourSwatch(upperHex) + " " + opt.label + " " + chalk.gray(upperHex),
97
148
  };
98
149
  });
99
150
 
151
+ let initial = Math.max(
152
+ 0,
153
+ choices.findIndex((choice) => choice.name === "custom"),
154
+ );
155
+
156
+ if (currentHexValue) {
157
+ const currentIndex = choices.findIndex(
158
+ (choice) => choice.name === currentHexValue,
159
+ );
160
+
161
+ if (currentIndex !== -1) {
162
+ initial = currentIndex;
163
+ } else {
164
+ choices.unshift({
165
+ name: "__current__",
166
+ message:
167
+ "Keep current " +
168
+ label +
169
+ " " +
170
+ colourSwatch(currentHexValue) +
171
+ " " +
172
+ chalk.gray(currentHexValue),
173
+ });
174
+ initial = 0;
175
+ }
176
+ } else if (defaultHexValue) {
177
+ const defaultIndex = choices.findIndex(
178
+ (choice) => choice.name === defaultHexValue,
179
+ );
180
+ if (defaultIndex !== -1) {
181
+ initial = defaultIndex;
182
+ }
183
+ }
184
+
100
185
  const selected = await new Select({
101
186
  name: label,
102
187
  message: label + " colour",
103
188
  choices,
189
+ initial,
104
190
  }).run();
105
191
 
192
+ if (selected === "__current__" && currentHexValue) return currentHexValue;
106
193
  if (selected !== "custom") return selected.toUpperCase();
107
194
 
108
- return askHex(label + "Custom", "Enter " + label + " hex", defaultHex);
195
+ const fallbackHex = currentHexValue || defaultHexValue || "#000000";
196
+ return askHex(label + "Custom", "Enter " + label + " hex", fallbackHex);
109
197
  }
110
198
 
111
199
  function hasFile(fileName) {
@@ -124,6 +212,50 @@ function readPackageJson() {
124
212
  }
125
213
  }
126
214
 
215
+ function readExistingConfig() {
216
+ const configPath = path.join(process.cwd(), "emily.config.json");
217
+
218
+ if (!fs.existsSync(configPath)) return null;
219
+
220
+ try {
221
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ function getFontInitialIndex(fontKey, fallbackIndex) {
228
+ if (!fontKey || typeof fontKey !== "string") return fallbackIndex;
229
+ const normalised = fontKey.toLowerCase();
230
+ const index = FONT_OPTIONS.findIndex((option) => option.name === normalised);
231
+ return index === -1 ? fallbackIndex : index;
232
+ }
233
+
234
+ function getExistingAdditionalColours(existingColours) {
235
+ if (!isPlainObject(existingColours)) return {};
236
+
237
+ const additional = {};
238
+ Object.entries(existingColours).forEach(([name, value]) => {
239
+ if (CORE_COLOUR_KEYS.has(name)) return;
240
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) return;
241
+
242
+ const upperHex = normaliseHex(value);
243
+ if (!upperHex) return;
244
+ additional[name] = upperHex;
245
+ });
246
+
247
+ return additional;
248
+ }
249
+
250
+ function getBaseUnitInitial(config) {
251
+ const rawBaseUnit = config && typeof config.baseUnit === "string"
252
+ ? config.baseUnit
253
+ : "";
254
+ const parsed = Number.parseInt(rawBaseUnit, 10);
255
+ if (Number.isNaN(parsed) || parsed <= 0) return "18";
256
+ return String(parsed);
257
+ }
258
+
127
259
  function hasDependency(packageJson, dependencyName) {
128
260
  if (!packageJson) return false;
129
261
 
@@ -344,6 +476,16 @@ function createDefaultConfig({
344
476
  sourceDir: detectedProject.sourceDir,
345
477
  sourceGlobs: detectedProject.sourceGlobs,
346
478
  ignore: DEFAULT_PURGE_IGNORE,
479
+ safelist: [
480
+ "bg-dark",
481
+ "text-dark",
482
+ "border-dark",
483
+ "fill-dark",
484
+ "bg-light",
485
+ "text-light",
486
+ "border-light",
487
+ "fill-light",
488
+ ],
347
489
  extensions: PURGE_EXTENSIONS,
348
490
  },
349
491
 
@@ -492,10 +634,24 @@ async function init() {
492
634
  const spinner = ora("Analysing project structure...").start();
493
635
  const detectedProject = detectProject();
494
636
  spinner.succeed("Detected project: " + chalk.cyan(detectedProject.name));
637
+ const existingConfig = readExistingConfig();
638
+ const existingColours = isPlainObject(existingConfig && existingConfig.colours)
639
+ ? existingConfig.colours
640
+ : {};
641
+
642
+ if (existingConfig) {
643
+ console.log(
644
+ chalk.gray(
645
+ " Found existing emily.config.json. Prompts are pre-filled from current settings.",
646
+ ),
647
+ );
648
+ }
495
649
 
496
650
  const packageJsonData = readPackageJson();
497
651
  const pkgName =
498
- packageJsonData && packageJsonData.name
652
+ existingConfig && typeof existingConfig.name === "string" && existingConfig.name.trim()
653
+ ? existingConfig.name.trim()
654
+ : packageJsonData && packageJsonData.name
499
655
  ? titleCasePackageName(packageJsonData.name)
500
656
  : "My Design System";
501
657
 
@@ -523,11 +679,13 @@ async function init() {
523
679
  "brand",
524
680
  COLOUR_PRESETS.primary,
525
681
  "#DB2777",
682
+ existingColours.brand,
526
683
  );
527
684
  const accent = await askColourFromPresets(
528
685
  "accent",
529
686
  COLOUR_PRESETS.secondary,
530
687
  "#2563EB",
688
+ existingColours.accent,
531
689
  );
532
690
 
533
691
  console.log(
@@ -549,16 +707,19 @@ async function init() {
549
707
  "success",
550
708
  COLOUR_PRESETS.success,
551
709
  "#017F65",
710
+ existingColours.success,
552
711
  );
553
712
  const warning = await askColourFromPresets(
554
713
  "warning",
555
714
  COLOUR_PRESETS.warning,
556
715
  "#FFC107",
716
+ existingColours.warning,
557
717
  );
558
718
  const error = await askColourFromPresets(
559
719
  "error",
560
720
  COLOUR_PRESETS.error,
561
721
  "#B20000",
722
+ existingColours.error,
562
723
  );
563
724
 
564
725
  const colours = {
@@ -569,7 +730,8 @@ async function init() {
569
730
  success,
570
731
  warning,
571
732
  error,
572
- neutral: "#57534E",
733
+ neutral: normaliseHex(existingColours.neutral) || "#57534E",
734
+ ...getExistingAdditionalColours(existingColours),
573
735
  };
574
736
 
575
737
  let addingMore = true;
@@ -619,14 +781,24 @@ async function init() {
619
781
  name: "headingFont",
620
782
  message: "Heading font",
621
783
  choices: FONT_OPTIONS,
622
- initial: 0,
784
+ initial: getFontInitialIndex(
785
+ isPlainObject(existingConfig && existingConfig.fontFamily)
786
+ ? existingConfig.fontFamily.heading
787
+ : existingConfig && existingConfig.fontFamily,
788
+ 0,
789
+ ),
623
790
  }).run();
624
791
 
625
792
  const bodyFont = await new Select({
626
793
  name: "bodyFont",
627
794
  message: "Body font",
628
795
  choices: FONT_OPTIONS,
629
- initial: 1,
796
+ initial: getFontInitialIndex(
797
+ isPlainObject(existingConfig && existingConfig.fontFamily)
798
+ ? existingConfig.fontFamily.body
799
+ : existingConfig && existingConfig.fontFamily,
800
+ 1,
801
+ ),
630
802
  }).run();
631
803
 
632
804
  // =========================================================================
@@ -636,7 +808,7 @@ async function init() {
636
808
  const baseUnitRaw = await new Input({
637
809
  name: "baseUnit",
638
810
  message: "Base spacing unit in px (label/documentation only)",
639
- initial: "18",
811
+ initial: getBaseUnitInitial(existingConfig),
640
812
  validate: function (value) {
641
813
  const parsed = Number.parseInt(value, 10);
642
814
 
@@ -675,7 +847,7 @@ async function init() {
675
847
  // BUILD
676
848
  // =========================================================================
677
849
 
678
- const config = createDefaultConfig({
850
+ const generatedDefaults = createDefaultConfig({
679
851
  name: projectName.trim(),
680
852
  colours,
681
853
  headingFont,
@@ -683,6 +855,19 @@ async function init() {
683
855
  baseUnit,
684
856
  detectedProject,
685
857
  });
858
+ const config = mergeWithDefaults(generatedDefaults, existingConfig);
859
+ config.name = projectName.trim();
860
+
861
+ if (!existingConfig || !existingConfig.description) {
862
+ config.description = config.name + " design system";
863
+ }
864
+
865
+ config.baseUnit = baseUnit + "px";
866
+ config.fontFamily = {
867
+ heading: headingFont,
868
+ body: bodyFont,
869
+ };
870
+ config.colours = colours;
686
871
 
687
872
  const configPath = path.join(process.cwd(), "emily.config.json");
688
873
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
package/src/migrate.js CHANGED
@@ -224,11 +224,14 @@ const UTILITY_PREFIX_ALLOWLIST = new Set([
224
224
  'content',
225
225
  'self',
226
226
  'place',
227
+ 'box',
227
228
  'object',
228
229
  'overflow',
230
+ 'overscroll',
229
231
  'divide',
230
232
  'cursor',
231
233
  'select',
234
+ 'transition',
232
235
  'duration',
233
236
  'delay',
234
237
  'ease',
@@ -241,6 +244,9 @@ const UTILITY_PREFIX_ALLOWLIST = new Set([
241
244
  'basis',
242
245
  'grow',
243
246
  'shrink',
247
+ 'color-scheme',
248
+ 'field-sizing',
249
+ 'scrollbar',
244
250
  ]);
245
251
 
246
252
  function hasUtilityLikeSyntax(className) {
@@ -579,7 +585,7 @@ function isLikelyUtilityClass(className) {
579
585
  * Ignore obvious prose / JS expressions
580
586
  */
581
587
  if (/[.]+$/.test(className)) return false;
582
- if (/^[[][^:\]]+[]]$/.test(className)) return false;
588
+ if (/^\[[^\]:]+\]$/.test(className)) return false;
583
589
  if (/^[a-zA-Z]+\.[a-zA-Z0-9_$]+$/.test(className)) return false;
584
590
  if (/^[a-z]+$/.test(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
585
591
  if (!hasUtilityLikeSyntax(className) && !SINGLE_WORD_UTILITY_ALLOWLIST.has(className)) return false;
package/src/purge.js CHANGED
@@ -222,6 +222,9 @@ function printFileSummary(files, extensions) {
222
222
  function purgeCSS(css, scanDir, config) {
223
223
  const resolvedPurgeConfig = resolvePurgeConfig(config);
224
224
  const extensions = resolvedPurgeConfig.extensions || DEFAULT_EXTENSIONS;
225
+ const safelist = Array.isArray(resolvedPurgeConfig.safelist)
226
+ ? resolvedPurgeConfig.safelist
227
+ : [];
225
228
  const files = getFilesForPurge(scanDir, config, extensions);
226
229
 
227
230
  printFileSummary(files, extensions);
@@ -245,7 +248,12 @@ function purgeCSS(css, scanDir, config) {
245
248
  }
246
249
  }
247
250
 
251
+ safelist.forEach((className) => usedClasses.add(className));
252
+
248
253
  console.log(` Extracted ${usedClasses.size} unique class names`);
254
+ if (safelist.length > 0) {
255
+ console.log(` Safelisted ${safelist.length} class names`);
256
+ }
249
257
 
250
258
  const blocks = extractBlocks(css);
251
259
  const purgedBlocks = blocks
@@ -170,12 +170,21 @@ function resolvePurgeConfig(config = {}, options = {}) {
170
170
  }
171
171
 
172
172
  const sourceDir = purge.sourceDir || preset.sourceDir || '.';
173
+ const safelist = Array.isArray(purge.safelist)
174
+ ? unique(
175
+ purge.safelist
176
+ .filter((entry) => typeof entry === 'string')
177
+ .map((entry) => entry.trim())
178
+ .filter(Boolean),
179
+ )
180
+ : [];
173
181
 
174
182
  return {
175
183
  projectType,
176
184
  sourceDir,
177
185
  sourceGlobs,
178
186
  ignore: unique([...DEFAULT_PURGE_IGNORE, ...(purge.ignore || [])]),
187
+ safelist,
179
188
  extensions: Array.isArray(purge.extensions) && purge.extensions.length > 0
180
189
  ? purge.extensions
181
190
  : DEFAULT_EXTENSIONS,
@@ -188,4 +197,3 @@ module.exports = {
188
197
  detectProjectType,
189
198
  resolvePurgeConfig,
190
199
  };
191
-