emily-css 1.2.7 → 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 +37 -1
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/generators/background.js +11 -0
- package/src/generators/display.js +2 -0
- package/src/generators/effects.js +2 -0
- package/src/generators/overflow.js +9 -0
- package/src/generators/positioning.js +14 -0
- package/src/generators/sizing.js +1 -0
- package/src/generators/transforms.js +7 -1
- package/src/index.js +38 -1
- package/src/init.js +195 -10
- package/src/migrate.js +6 -0
- package/src/purge.js +8 -0
- package/src/purgeConfig.js +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ 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
|
+
|
|
7
43
|
## v1.2.7 — May 2026
|
|
8
44
|
|
|
9
45
|
**Fix migration scanner filtering so Vue dynamic class placeholders such as [rootClass] are not reported as unsupported arbitrary value utilities.**
|
|
@@ -463,4 +499,4 @@ All notable changes to `emily-css` are documented here.
|
|
|
463
499
|
- 11,844 utility classes generated from `emily.config.json`
|
|
464
500
|
- OKLCH colour scale generation — one hex in, 10-shade scale out
|
|
465
501
|
- Responsive variants (`sm:` `md:` `lg:` `xl:` `2xl:`)
|
|
466
|
-
- 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
|
@@ -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
|
}
|
|
@@ -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`;
|
package/src/generators/sizing.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
145
|
+
name: upperHex,
|
|
95
146
|
message:
|
|
96
|
-
colourSwatch(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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) {
|
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
|
package/src/purgeConfig.js
CHANGED
|
@@ -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
|
-
|