emily-css 1.0.8 → 1.0.9

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/bin/emilyui.js CHANGED
@@ -6,16 +6,19 @@ if (command === "init") {
6
6
  require("../src/init.js");
7
7
  } else if (command === "build") {
8
8
  const { build } = require("../src/index.js");
9
- build();
9
+ build({ keepFull: process.argv.includes("--keep-full") });
10
10
  } else if (command === "purge") {
11
11
  require("../src/purge-cmd.js");
12
+ } else if (command === "watch") {
13
+ require("../src/watch.js");
12
14
  } else {
13
15
  console.log(`
14
16
  emily-css - Config-driven CSS framework generator
15
17
 
16
18
  Usage:
17
19
  emily-css init Set up a new project
18
- emily-css build Generate emily.css from your config
19
- emily-css purge Remove unused utilities for production
20
+ emily-css build Generate production CSS: dist/emily.min.css
21
+ emily-css watch Dev mode: rebuild full CSS only when needed
22
+ emily-css purge Advanced: manually purge unused utilities
20
23
  `);
21
24
  }
package/package.json CHANGED
@@ -1,40 +1,49 @@
1
- {
2
- "name": "emily-css",
3
- "version": "1.0.8",
4
- "description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
5
- "main": "src/index.js",
6
- "bin": {
7
- "emily-css": "./bin/emilyui.js"
8
- },
9
- "files": [
10
- "bin/",
11
- "src/",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "scripts": {
16
- "build": "node src/index.js",
17
- "dev": "nodemon src/index.js",
18
- "init": "node src/init.js",
19
- "test": "node tests/test.js"
20
- },
21
- "keywords": [
22
- "css",
23
- "design-system",
24
- "components",
25
- "config-driven",
26
- "utility-css",
27
- "accessibility",
28
- "drupal",
29
- "legacy",
30
- "no-build-step"
31
- ],
32
- "author": "Andy Terry",
33
- "license": "MIT",
34
- "engines": {
35
- "node": ">=16.0.0"
36
- },
37
- "devDependencies": {
38
- "nodemon": "^3.0.0"
39
- }
40
- }
1
+ {
2
+ "name": "emily-css",
3
+ "version": "1.0.9",
4
+ "description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "emily-css": "bin/emilyui.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "node src/index.js",
17
+ "dev": "nodemon src/index.js",
18
+ "init": "node src/init.js",
19
+ "test": "node tests/test.js"
20
+ },
21
+ "keywords": [
22
+ "css",
23
+ "design-system",
24
+ "components",
25
+ "config-driven",
26
+ "utility-css",
27
+ "accessibility",
28
+ "drupal",
29
+ "legacy",
30
+ "no-build-step"
31
+ ],
32
+ "author": "Andy Terry",
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=16.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "nodemon": "^3.0.0"
39
+ },
40
+ "dependencies": {
41
+ "boxen": "^5.1.2",
42
+ "chalk": "^4.1.2",
43
+ "chokidar": "^5.0.0",
44
+ "cross-spawn": "^7.0.6",
45
+ "emily-css": "^1.0.8",
46
+ "enquirer": "^2.4.1",
47
+ "ora": "^5.4.1"
48
+ }
49
+ }
package/src/index.js CHANGED
@@ -779,7 +779,7 @@ function addStateVariants(css) {
779
779
  // BUILD FUNCTION
780
780
  // ============================================================================
781
781
 
782
- function build(options = {}) {
782
+ function buildFullFramework() {
783
783
  const configPath = path.join(process.cwd(), 'emily.config.json');
784
784
  if (!fs.existsSync(configPath)) {
785
785
  console.error(`\n emily-css: No config found.\n Expected: ${configPath}\n Run "emily-css init" to create one.\n`);
@@ -787,24 +787,7 @@ function build(options = {}) {
787
787
  }
788
788
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
789
789
 
790
- if (options.purge) {
791
- const { purgeCSS } = require('./purge.js');
792
- const cssPath = path.join(process.cwd(), 'dist/emily.css');
793
- if (!fs.existsSync(cssPath)) {
794
- console.error(' emily-css: Run "emily-css build" first.');
795
- process.exit(1);
796
- }
797
- console.log(`Purging unused utilities from ${options.purge}...`);
798
- const css = fs.readFileSync(cssPath, 'utf8');
799
- const purged = purgeCSS(css, options.purge);
800
- const minified = purged.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s?\{/g, '{').replace(/\s?\}/g, '}').replace(/;\s/g, ';').trim();
801
- fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.css'), purged);
802
- fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.min.css'), minified);
803
- console.log('✓ Purged CSS: dist/emily.purged.css');
804
- return;
805
- }
806
-
807
- console.log('Building EmilyUI...');
790
+ console.log('Building EmilyCSS full framework...');
808
791
 
809
792
  // Generate colours
810
793
  const colours = generateAllColours(config.colours);
@@ -926,26 +909,103 @@ ${bodyFont}`;
926
909
  fs.writeFileSync(outputPath, css);
927
910
  console.log(`✓ Generated CSS: ${outputPath}`);
928
911
  console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
912
+ console.log('Full framework build complete');
913
+ }
914
+
915
+ function minify(css) {
916
+ return css
917
+ .replace(/\/\*[\s\S]*?\*\//g, '')
918
+ .replace(/\s+/g, ' ')
919
+ .replace(/\s?\{/g, '{')
920
+ .replace(/\s?\}/g, '}')
921
+ .replace(/;\s/g, ';')
922
+ .trim();
923
+ }
924
+
925
+ function getConfig() {
926
+ const configPath = path.join(process.cwd(), 'emily.config.json');
929
927
 
930
- // Generate minified version
931
- const minified = css.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s?\{/g, '{').replace(/\s?\}/g, '}').replace(/;\s/g, ';').trim();
928
+ if (!fs.existsSync(configPath)) {
929
+ console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
930
+ process.exit(1);
931
+ }
932
+
933
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
934
+ }
935
+
936
+ function getSourceDir(config) {
937
+ return config.purge && config.purge.sourceDir ? config.purge.sourceDir : '.';
938
+ }
939
+
940
+ function buildProductionCss() {
941
+ const config = getConfig();
942
+ const sourceDir = getSourceDir(config);
943
+ const cssPath = path.join(process.cwd(), 'dist/emily.css');
932
944
  const minPath = path.join(process.cwd(), 'dist/emily.min.css');
945
+
946
+ if (!fs.existsSync(cssPath)) {
947
+ buildFullFramework();
948
+ }
949
+
950
+ const { purgeCSS } = require('./purge.js');
951
+ const css = fs.readFileSync(cssPath, 'utf8');
952
+ const purged = purgeCSS(css, sourceDir, config);
953
+ const minified = minify(purged);
954
+
933
955
  fs.writeFileSync(minPath, minified);
934
- console.log(' -> Minified: ' + minPath);
935
- console.log(' -> File size: ' + (minified.length / 1024).toFixed(2) + ' KB (minified)');
956
+
957
+ return {
958
+ css,
959
+ purged,
960
+ minified,
961
+ originalSize: Buffer.byteLength(css, 'utf8'),
962
+ outputSize: Buffer.byteLength(minified, 'utf8')
963
+ };
964
+ }
965
+
966
+ function isFrameworkStale() {
967
+ const configPath = path.join(process.cwd(), 'emily.config.json');
968
+ const cssPath = path.join(process.cwd(), 'dist/emily.css');
969
+
970
+ if (!fs.existsSync(cssPath)) return true;
971
+ if (!fs.existsSync(configPath)) return true;
972
+
973
+ return fs.statSync(configPath).mtimeMs > fs.statSync(cssPath).mtimeMs;
974
+ }
975
+
976
+ function ensureFullFramework() {
977
+ if (isFrameworkStale()) {
978
+ buildFullFramework();
979
+ }
980
+ }
981
+
982
+ function build(options = {}) {
983
+ ensureFullFramework();
984
+
985
+ const result = buildProductionCss();
986
+ const cssPath = path.join(process.cwd(), 'dist/emily.css');
987
+
988
+ console.log('✓ Generated production CSS: dist/emily.min.css');
989
+ console.log('✓ File size: ' + (result.outputSize / 1024).toFixed(2) + ' KB');
990
+
991
+ if (!options.keepFull && fs.existsSync(cssPath)) {
992
+ fs.unlinkSync(cssPath);
993
+ console.log('Removed dist/emily.css for production build.');
994
+ }
936
995
 
937
996
  console.log('Build complete');
938
997
  }
939
998
 
940
999
  if (require.main === module) {
941
1000
  const args = process.argv.slice(2);
942
- const purgeIndex = args.indexOf('--purge');
943
- const purgeDir = purgeIndex !== -1 ? args[purgeIndex + 1] : null;
944
- build(purgeDir ? { purge: purgeDir } : {});
1001
+ build({ keepFull: args.includes('--keep-full') });
945
1002
  }
946
1003
 
947
1004
  module.exports = {
948
1005
  build,
1006
+ buildFullFramework,
1007
+ buildProductionCss,
1008
+ ensureFullFramework,
949
1009
  hexToOklch,
950
1010
  oklchToHex,
951
1011
  generateColourScale,
package/src/init.js CHANGED
@@ -1,256 +1,667 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const readline = require('readline');
4
-
5
- const rl = readline.createInterface({
6
- input: process.stdin,
7
- output: process.stdout
8
- });
9
-
10
- // Helper function for prompts
11
- function prompt(question) {
12
- return new Promise(resolve => {
13
- rl.question(question, answer => {
14
- resolve(answer);
15
- });
16
- });
17
- }
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const crossSpawn = require("cross-spawn");
4
+ const { Form, Select, Input } = require("enquirer");
5
+ const chalk = require("chalk");
6
+ const ora = require("ora");
7
+ const boxen = require("boxen");
8
+
9
+ // ============================================================================
10
+ // CONSTANTS
11
+ // ============================================================================
12
+
13
+ const DEFAULT_PURGE_IGNORE = [
14
+ "node_modules",
15
+ ".git",
16
+ ".nuxt",
17
+ ".next",
18
+ ".output",
19
+ "dist",
20
+ "build",
21
+ "coverage",
22
+ ".cache",
23
+ ".vite",
24
+ ];
25
+
26
+ const DEFAULT_COLOURS = {
27
+ primary: "#DB2777",
28
+ secondary: "#2563EB",
29
+ success: "#017F65",
30
+ warning: "#FFC107",
31
+ error: "#B20000",
32
+ neutral: "#57534E",
33
+ };
34
+
35
+ const COLOUR_PRESETS = {
36
+ primary: [
37
+ { value: "#DB2777", label: "Emily Pink" },
38
+ { value: "#114B5F", label: "Deep Teal" },
39
+ { value: "#2563EB", label: "Blue" },
40
+ { value: "#017F65", label: "Green" },
41
+ { value: "custom", label: "Custom hex" },
42
+ ],
43
+ secondary: [
44
+ { value: "#2563EB", label: "Blue" },
45
+ { value: "#028090", label: "Teal" },
46
+ { value: "#7C3AED", label: "Purple" },
47
+ { value: "#DB2777", label: "Emily Pink" },
48
+ { value: "custom", label: "Custom hex" },
49
+ ],
50
+ success: [
51
+ { value: "#017F65", label: "Accessible Green" },
52
+ { value: "#15803D", label: "Forest Green" },
53
+ { value: "#50C878", label: "Emerald" },
54
+ { value: "custom", label: "Custom hex" },
55
+ ],
56
+ warning: [
57
+ { value: "#FFC107", label: "Amber" },
58
+ { value: "#F59E0B", label: "Orange" },
59
+ { value: "#FFBF00", label: "Yellow" },
60
+ { value: "custom", label: "Custom hex" },
61
+ ],
62
+ error: [
63
+ { value: "#B20000", label: "Accessible Red" },
64
+ { value: "#DC2626", label: "Red" },
65
+ { value: "#F45B69", label: "Coral" },
66
+ { value: "custom", label: "Custom hex" },
67
+ ],
68
+ neutral: [
69
+ { value: "#57534E", label: "Warm Grey" },
70
+ { value: "#334155", label: "Slate" },
71
+ { value: "#111827", label: "Near Black" },
72
+ { value: "custom", label: "Custom hex" },
73
+ ],
74
+ };
75
+
76
+ const FONT_OPTIONS = [
77
+ { name: "lexend", message: "Lexend" },
78
+ { name: "inter", message: "Inter" },
79
+ { name: "system", message: "System sans" },
80
+ { name: "georgia", message: "Georgia" },
81
+ { name: "mono", message: "Monospace" },
82
+ ];
83
+
84
+ const PURGE_EXTENSIONS = [
85
+ ".html",
86
+ ".htm",
87
+ ".twig",
88
+ ".njk",
89
+ ".liquid",
90
+ ".hbs",
91
+ ".jsx",
92
+ ".tsx",
93
+ ".vue",
94
+ ".php",
95
+ ".astro",
96
+ ".svelte",
97
+ ".blade.php",
98
+ ".jinja",
99
+ ".jinja2",
100
+ ".j2",
101
+ ".md",
102
+ ];
103
+
104
+ // ============================================================================
105
+ // HELPERS
106
+ // ============================================================================
18
107
 
19
- // Validate hex colour
20
108
  function isValidHex(hex) {
21
109
  return /^#[0-9A-F]{6}$/i.test(hex);
22
110
  }
23
111
 
24
- // Default colours
25
- const defaultColours = {
26
- primary: '#DB2777',
27
- secondary: '#2563EB',
28
- success: '#017F65',
29
- warning: '#ffc107',
30
- error: '#b20000',
31
- neutral: '#57534E'
32
- };
112
+ function colourChoice(hex, label) {
113
+ if (hex === "custom") {
114
+ return {
115
+ name: "custom",
116
+ message: "Custom hex",
117
+ };
118
+ }
119
+
120
+ return {
121
+ name: hex,
122
+ message: `${chalk.hex(hex)("■")} ${label} ${chalk.gray(hex)}`,
123
+ };
124
+ }
125
+
126
+ async function askColour(colourName) {
127
+ const choices = COLOUR_PRESETS[colourName].map((option) =>
128
+ colourChoice(option.value, option.label),
129
+ );
130
+
131
+ const selected = await new Select({
132
+ name: colourName,
133
+ message: `${colourName} colour`,
134
+ choices,
135
+ }).run();
136
+
137
+ if (selected !== "custom") {
138
+ return selected.toUpperCase();
139
+ }
140
+
141
+ const custom = await new Input({
142
+ name: `${colourName}Custom`,
143
+ message: `Enter custom ${colourName} hex`,
144
+ initial: DEFAULT_COLOURS[colourName],
145
+ validate(value) {
146
+ return isValidHex(value)
147
+ ? true
148
+ : "Enter a valid hex colour, for example #0077B6";
149
+ },
150
+ }).run();
151
+
152
+ return custom.toUpperCase();
153
+ }
154
+
155
+ function hasFile(fileName) {
156
+ return fs.existsSync(path.join(process.cwd(), fileName));
157
+ }
158
+
159
+ function readPackageJson() {
160
+ const packagePath = path.join(process.cwd(), "package.json");
161
+
162
+ if (!fs.existsSync(packagePath)) {
163
+ return null;
164
+ }
165
+
166
+ try {
167
+ return JSON.parse(fs.readFileSync(packagePath, "utf8"));
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function hasDependency(packageJson, dependencyName) {
174
+ if (!packageJson) return false;
175
+
176
+ return Boolean(
177
+ packageJson.dependencies?.[dependencyName] ||
178
+ packageJson.devDependencies?.[dependencyName],
179
+ );
180
+ }
181
+
182
+ function addEmilyScriptsToPackageJson() {
183
+ const packagePath = path.join(process.cwd(), "package.json");
184
+
185
+ if (!fs.existsSync(packagePath)) {
186
+ return false;
187
+ }
188
+
189
+ try {
190
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
191
+
192
+ packageJson.scripts = packageJson.scripts || {};
193
+
194
+ let changed = false;
195
+
196
+ if (!packageJson.scripts["emily:build"]) {
197
+ packageJson.scripts["emily:build"] = "emily-css build";
198
+ changed = true;
199
+ }
200
+
201
+ if (!packageJson.scripts["emily:watch"]) {
202
+ packageJson.scripts["emily:watch"] = "emily-css watch";
203
+ changed = true;
204
+ }
205
+
206
+ if (changed) {
207
+ fs.writeFileSync(
208
+ packagePath,
209
+ JSON.stringify(packageJson, null, 2) + "\n",
210
+ );
211
+ }
212
+
213
+ return true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ // ============================================================================
220
+ // PROJECT DETECTION
221
+ // ============================================================================
222
+
223
+ function detectProject() {
224
+ const packageJson = readPackageJson();
225
+
226
+ if (
227
+ hasFile("nuxt.config.ts") ||
228
+ hasFile("nuxt.config.js") ||
229
+ hasDependency(packageJson, "nuxt")
230
+ ) {
231
+ return {
232
+ name: "Nuxt",
233
+ sourceDir: ".",
234
+ sourceGlobs: [
235
+ "./components/**/*.{vue,js,ts}",
236
+ "./pages/**/*.vue",
237
+ "./layouts/**/*.vue",
238
+ "./app.vue",
239
+ ],
240
+ };
241
+ }
242
+
243
+ if (hasDependency(packageJson, "next")) {
244
+ return {
245
+ name: "Next.js",
246
+ sourceDir: ".",
247
+ sourceGlobs: [
248
+ "./app/**/*.{js,jsx,ts,tsx}",
249
+ "./pages/**/*.{js,jsx,ts,tsx}",
250
+ "./components/**/*.{js,jsx,ts,tsx}",
251
+ "./src/**/*.{js,jsx,ts,tsx}",
252
+ ],
253
+ };
254
+ }
255
+
256
+ if (hasDependency(packageJson, "react")) {
257
+ return {
258
+ name: "React",
259
+ sourceDir: "./src",
260
+ sourceGlobs: [
261
+ "./src/**/*.{js,jsx,ts,tsx}",
262
+ "./components/**/*.{js,jsx,ts,tsx}",
263
+ ],
264
+ };
265
+ }
266
+
267
+ if (
268
+ hasDependency(packageJson, "vue") ||
269
+ hasFile("vite.config.ts") ||
270
+ hasFile("vite.config.js")
271
+ ) {
272
+ return {
273
+ name: "Vue/Vite",
274
+ sourceDir: "./src",
275
+ sourceGlobs: ["./src/**/*.{vue,js,ts}"],
276
+ };
277
+ }
278
+
279
+ if (hasDependency(packageJson, "astro") || hasFile("astro.config.mjs")) {
280
+ return {
281
+ name: "Astro",
282
+ sourceDir: "./src",
283
+ sourceGlobs: ["./src/**/*.{astro,html,js,ts,vue,jsx,tsx,svelte}"],
284
+ };
285
+ }
286
+
287
+ const rootFiles = fs.readdirSync(process.cwd());
288
+ const hasDrupalInfoFile = rootFiles.some((file) =>
289
+ file.endsWith(".info.yml"),
290
+ );
291
+
292
+ if (
293
+ hasDrupalInfoFile ||
294
+ fs.existsSync(path.join(process.cwd(), "web/core"))
295
+ ) {
296
+ return {
297
+ name: "Drupal",
298
+ sourceDir: ".",
299
+ sourceGlobs: [
300
+ "./web/themes/custom/**/*.{twig,js,ts}",
301
+ "./templates/**/*.html.twig",
302
+ "./components/**/*.twig",
303
+ "./**/*.theme",
304
+ ],
305
+ };
306
+ }
33
307
 
34
- // Default config template - matches emily.config.json structure
35
- function createDefaultConfig(name, colours, fonts, baseUnit, sourceDir) {
308
+ return {
309
+ name: "Static/Generic",
310
+ sourceDir: ".",
311
+ sourceGlobs: [
312
+ "./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,js,ts}",
313
+ ],
314
+ };
315
+ }
316
+
317
+ // ============================================================================
318
+ // CONFIG BUILDER
319
+ // ============================================================================
320
+
321
+ function createDefaultConfig({
322
+ name,
323
+ colours,
324
+ headingFont,
325
+ bodyFont,
326
+ monoFont,
327
+ baseUnit,
328
+ detectedProject,
329
+ sourceDir,
330
+ }) {
36
331
  return {
37
332
  name,
38
333
  description: `${name} design system`,
334
+
39
335
  baseUnit: `${baseUnit}px`,
40
- baseFontSize: '16px',
41
- fontFamily: { heading: 'lexend', body: 'inter' },
336
+ baseFontSize: "16px",
337
+
338
+ fontFamily: {
339
+ heading: headingFont,
340
+ body: bodyFont,
341
+ mono: monoFont,
342
+ },
343
+
42
344
  customFonts: [],
345
+
43
346
  colours,
347
+
348
+ purge: {
349
+ projectType: detectedProject.name,
350
+ sourceDir,
351
+ sourceGlobs: detectedProject.sourceGlobs,
352
+ ignore: DEFAULT_PURGE_IGNORE,
353
+ extensions: PURGE_EXTENSIONS,
354
+ },
355
+
44
356
  breakpoints: {
45
- sm: '640px',
46
- md: '768px',
47
- lg: '1024px',
48
- xl: '1280px',
49
- '2xl': '1536px'
357
+ sm: "640px",
358
+ md: "768px",
359
+ lg: "1024px",
360
+ xl: "1280px",
361
+ "2xl": "1536px",
50
362
  },
363
+
51
364
  spacing: {
52
365
  scale: {
53
- '0': '0px',
54
- 'px': '1px',
55
- '0.5': '0.125rem',
56
- '1': '0.25rem',
57
- '1.5': '0.375rem',
58
- '2': '0.5rem',
59
- '2.5': '0.625rem',
60
- '3': '0.75rem',
61
- '3.5': '0.875rem',
62
- '4': '1rem',
63
- '5': '1.25rem',
64
- '6': '1.5rem',
65
- '7': '1.75rem',
66
- '8': '2rem',
67
- '9': '2.25rem',
68
- '10': '2.5rem',
69
- '11': '2.75rem',
70
- '12': '3rem',
71
- '14': '3.5rem',
72
- '16': '4rem',
73
- '20': '5rem',
74
- '24': '6rem',
75
- '28': '7rem',
76
- '32': '8rem',
77
- '36': '9rem',
78
- '40': '10rem',
79
- '44': '11rem',
80
- '48': '12rem',
81
- '52': '13rem',
82
- '56': '14rem',
83
- '60': '15rem',
84
- '64': '16rem',
85
- '72': '18rem',
86
- '80': '20rem',
87
- '96': '24rem'
366
+ 0: "0px",
367
+ px: "1px",
368
+ 0.5: "0.125rem",
369
+ 1: "0.25rem",
370
+ 1.5: "0.375rem",
371
+ 2: "0.5rem",
372
+ 2.5: "0.625rem",
373
+ 3: "0.75rem",
374
+ 3.5: "0.875rem",
375
+ 4: "1rem",
376
+ 5: "1.25rem",
377
+ 6: "1.5rem",
378
+ 7: "1.75rem",
379
+ 8: "2rem",
380
+ 9: "2.25rem",
381
+ 10: "2.5rem",
382
+ 11: "2.75rem",
383
+ 12: "3rem",
384
+ 14: "3.5rem",
385
+ 16: "4rem",
386
+ 20: "5rem",
387
+ 24: "6rem",
388
+ 28: "7rem",
389
+ 32: "8rem",
390
+ 36: "9rem",
391
+ 40: "10rem",
392
+ 44: "11rem",
393
+ 48: "12rem",
394
+ 52: "13rem",
395
+ 56: "14rem",
396
+ 60: "15rem",
397
+ 64: "16rem",
398
+ 72: "18rem",
399
+ 80: "20rem",
400
+ 96: "24rem",
88
401
  },
402
+
89
403
  borderWidths: [0, 2, 4, 8],
404
+
90
405
  borderRadius: {
91
- 'none': '0',
92
- 'sm': '4px',
93
- 'base': '8px',
94
- 'md': '12px',
95
- 'lg': '16px',
96
- 'full': '9999px'
97
- }
406
+ none: "0",
407
+ sm: "4px",
408
+ base: "8px",
409
+ md: "12px",
410
+ lg: "16px",
411
+ full: "9999px",
412
+ },
98
413
  },
414
+
99
415
  typography: {
100
416
  lineHeightRatio: 1.5,
417
+
101
418
  fontWeights: {
102
419
  light: 300,
103
420
  normal: 400,
104
421
  medium: 500,
105
422
  semibold: 600,
106
- bold: 700
423
+ bold: 700,
107
424
  },
425
+
108
426
  fontSizes: [
109
- { name: 'xs', value: '12px', lineHeight: 1.5 },
110
- { name: 'sm', value: '14px', lineHeight: 1.5 },
111
- { name: 'base', value: '16px', lineHeight: 1.6 },
112
- { name: 'lg', value: '18px', lineHeight: 1.6 },
113
- { name: 'xl', value: '20px', lineHeight: 1.6 },
114
- { name: '2xl', value: '24px', lineHeight: 1.4 },
115
- { name: '3xl', value: '30px', lineHeight: 1.4 },
116
- { name: '4xl', value: '36px', lineHeight: 1.3 }
117
- ]
427
+ { name: "xs", value: "12px", lineHeight: 1.5 },
428
+ { name: "sm", value: "14px", lineHeight: 1.5 },
429
+ { name: "base", value: "16px", lineHeight: 1.6 },
430
+ { name: "lg", value: "18px", lineHeight: 1.6 },
431
+ { name: "xl", value: "20px", lineHeight: 1.6 },
432
+ { name: "2xl", value: "24px", lineHeight: 1.4 },
433
+ { name: "3xl", value: "30px", lineHeight: 1.4 },
434
+ { name: "4xl", value: "36px", lineHeight: 1.3 },
435
+ ],
118
436
  },
437
+
119
438
  shadows: {
120
- sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
121
- base: '0 4px 6px rgba(0, 0, 0, 0.1)',
122
- md: '0 10px 15px rgba(0, 0, 0, 0.1)',
123
- lg: '0 20px 25px rgba(0, 0, 0, 0.15)',
124
- none: 'none'
439
+ sm: "0 1px 2px rgba(0, 0, 0, 0.05)",
440
+ base: "0 4px 6px rgba(0, 0, 0, 0.1)",
441
+ md: "0 10px 15px rgba(0, 0, 0, 0.1)",
442
+ lg: "0 20px 25px rgba(0, 0, 0, 0.15)",
443
+ none: "none",
125
444
  },
445
+
126
446
  transitions: {
127
- fast: '100ms',
128
- base: '200ms',
129
- slow: '300ms',
130
- timing: 'cubic-bezier(0.4, 0, 0.2, 1)'
447
+ fast: "100ms",
448
+ base: "200ms",
449
+ slow: "300ms",
450
+ timing: "cubic-bezier(0.4, 0, 0.2, 1)",
131
451
  },
452
+
132
453
  zIndex: {
133
- auto: 'auto',
134
- 0: '0',
135
- 10: '10',
136
- 20: '20',
137
- 30: '30',
138
- 40: '40',
139
- 50: '50',
140
- dropdown: '1000',
141
- sticky: '1020',
142
- fixed: '1030',
143
- modal: '1040',
144
- popover: '1060',
145
- tooltip: '1070'
454
+ auto: "auto",
455
+ 0: "0",
456
+ 10: "10",
457
+ 20: "20",
458
+ 30: "30",
459
+ 40: "40",
460
+ 50: "50",
461
+ dropdown: "1000",
462
+ sticky: "1020",
463
+ fixed: "1030",
464
+ modal: "1040",
465
+ popover: "1060",
466
+ tooltip: "1070",
146
467
  },
147
- opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100]
468
+
469
+ opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100],
148
470
  };
149
471
  }
150
472
 
473
+ // ============================================================================
474
+ // INIT
475
+ // ============================================================================
476
+
151
477
  async function init() {
152
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
153
- console.log(' EmilyUI Setup');
154
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
478
+ console.log(
479
+ chalk.bold.magenta("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"),
480
+ );
481
+ console.log(chalk.bold.magenta(" EmilyUI Setup"));
482
+ console.log(
483
+ chalk.bold.magenta("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"),
484
+ );
155
485
 
156
486
  try {
157
- // 1. Project name
158
- const name = await prompt('Project name: ');
159
- if (!name.trim()) {
160
- console.log('❌ Project name is required');
161
- rl.close();
162
- return;
487
+ const spinner = ora("Analysing project structure...").start();
488
+ const detectedProject = detectProject();
489
+ spinner.succeed(`Detected project: ${chalk.cyan(detectedProject.name)}`);
490
+
491
+ const { projectName } = await new Form({
492
+ name: "project",
493
+ message: "Project details",
494
+ choices: [
495
+ {
496
+ name: "projectName",
497
+ message: "Project name",
498
+ initial: "My Design System",
499
+ },
500
+ ],
501
+ }).run();
502
+
503
+ if (!projectName || !projectName.trim()) {
504
+ console.log(chalk.red("\nProject name is required.\n"));
505
+ process.exit(1);
163
506
  }
164
507
 
165
- // 2. Brand colours
166
- console.log('\nBrand colours (hex format, e.g., #0077b6):');
508
+ console.log(chalk.bold(`\n${chalk.magenta("→")} Brand colours`));
509
+
510
+ const normalisedColours = {};
511
+
512
+ for (const colourName of Object.keys(DEFAULT_COLOURS)) {
513
+ normalisedColours[colourName] = await askColour(colourName);
514
+ }
515
+
516
+ console.log(chalk.bold(`\n${chalk.magenta("→")} Typography`));
517
+
518
+ const headingFont = await new Select({
519
+ name: "headingFont",
520
+ message: "Heading font",
521
+ choices: FONT_OPTIONS,
522
+ initial: 0,
523
+ }).run();
167
524
 
168
- const colours = {};
169
- const colourNames = ['primary', 'secondary', 'success', 'warning', 'error', 'neutral'];
525
+ const bodyFont = await new Select({
526
+ name: "bodyFont",
527
+ message: "Body font",
528
+ choices: FONT_OPTIONS,
529
+ initial: 1,
530
+ }).run();
170
531
 
171
- for (const colourName of colourNames) {
172
- let colour;
173
- let valid = false;
532
+ const monoFont = await new Select({
533
+ name: "monoFont",
534
+ message: "Monospace font",
535
+ choices: FONT_OPTIONS,
536
+ initial: 4,
537
+ }).run();
174
538
 
175
- while (!valid) {
176
- colour = await prompt(` ${colourName} [${defaultColours[colourName]}]: `);
177
- colour = colour || defaultColours[colourName];
539
+ const { baseUnitInput } = await new Form({
540
+ name: "spacing",
541
+ message: "Spacing",
542
+ choices: [
543
+ {
544
+ name: "baseUnitInput",
545
+ message: "Base spacing unit in px",
546
+ initial: "8",
547
+ },
548
+ ],
549
+ validate(values) {
550
+ const parsed = Number.parseInt(values.baseUnitInput, 10);
178
551
 
179
- if (isValidHex(colour)) {
180
- colours[colourName] = colour;
181
- valid = true;
182
- } else {
183
- console.log(` ❌ Invalid hex colour. Use format: #0077b6`);
552
+ if (Number.isNaN(parsed) || parsed <= 0) {
553
+ return "Base spacing unit must be a positive number.";
184
554
  }
185
- }
186
- }
187
555
 
188
- // 3. Fonts
189
- console.log('\nFont families (optional, press Enter to skip):');
556
+ return true;
557
+ },
558
+ }).run();
190
559
 
191
- const fonts = {
192
- sans: await prompt(' Sans-serif font: ') || 'system-ui, -apple-system, sans-serif',
193
- serif: await prompt(' Serif font: ') || 'Georgia, serif',
194
- mono: await prompt(' Monospace font: ') || 'Menlo, Monaco, monospace'
195
- };
560
+ const baseUnit = Number.parseInt(baseUnitInput, 10);
196
561
 
197
- // 4. Base unit
198
- let baseUnit = 8;
199
- const baseUnitInput = await prompt('\nBase spacing unit (px) [8]: ');
200
- if (baseUnitInput.trim()) {
201
- const parsed = parseInt(baseUnitInput);
202
- if (!isNaN(parsed) && parsed > 0) {
203
- baseUnit = parsed;
204
- } else {
205
- console.log(' ⚠️ Invalid number, using default: 8px');
206
- }
207
- }
562
+ console.log(chalk.bold(`\n${chalk.magenta("→")} Purge settings`));
208
563
 
209
- // 5. Source directory for purge
210
- console.log('\nSource directory for purge (where your templates/HTML live):');
211
- let sourceDir = await prompt(' Source directory [./src]: ');
212
- sourceDir = sourceDir.trim() || './src';
564
+ const { sourceDir } = await new Form({
565
+ name: "paths",
566
+ message: `Detected ${detectedProject.name} project`,
567
+ choices: [
568
+ {
569
+ name: "sourceDir",
570
+ message: "Scan directory",
571
+ initial: detectedProject.sourceDir,
572
+ },
573
+ ],
574
+ }).run();
213
575
 
214
- // 6. Create config
215
- const config = createDefaultConfig(name, colours, fonts, baseUnit, sourceDir);
576
+ const config = createDefaultConfig({
577
+ name: projectName.trim(),
578
+ colours: normalisedColours,
579
+ headingFont,
580
+ bodyFont,
581
+ monoFont,
582
+ baseUnit,
583
+ detectedProject,
584
+ sourceDir: sourceDir.trim() || detectedProject.sourceDir,
585
+ });
216
586
 
217
- // 7. Write config file to the user's project directory
218
- const configPath = path.join(process.cwd(), 'emily.config.json');
587
+ const configPath = path.join(process.cwd(), "emily.config.json");
219
588
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
220
589
 
221
- console.log('\n✅ Configuration created!');
222
- console.log(` Project: ${name}`);
223
- console.log(` Primary colour: ${colours.primary}`);
224
- console.log(` Base unit: ${baseUnit}px`);
225
- console.log(`\n📝 Config saved: ${configPath}`);
590
+ console.log("");
591
+ const buildSpinner = ora("Building EmilyUI CSS...").start();
226
592
 
227
- // 8. Run build
228
- console.log('\n🔨 Building CSS...\n');
229
- rl.close();
230
-
231
- // Spawn build process
232
- const { spawn } = require('child_process');
233
- const build = spawn('npx', ['emily-css', 'build'], {
593
+ const build = crossSpawn("npx", ["emily-css", "build"], {
234
594
  cwd: process.cwd(),
235
- stdio: 'inherit'
595
+ stdio: "pipe",
596
+ shell: process.platform === "win32",
597
+ });
598
+
599
+ let stderr = "";
600
+
601
+ build.stderr.on("data", (data) => {
602
+ stderr += data.toString();
236
603
  });
237
604
 
238
- build.on('close', code => {
605
+ build.on("close", (code) => {
239
606
  if (code === 0) {
240
- console.log('\n✅ Setup complete!');
241
- console.log('\n💡 Next steps:');
242
- console.log(' 1. Open showcase.html in your browser to see components');
243
- console.log(' 2. Copy component code into your project');
244
- console.log(' 3. Update emily.config.json to customize colours/fonts');
245
- console.log(' 4. Run: emily-css purge (to remove unused CSS for production)');
607
+ buildSpinner.succeed("EmilyUI CSS built successfully.");
608
+
609
+ const scriptsAdded = addEmilyScriptsToPackageJson();
610
+
611
+ console.log(
612
+ "\n" +
613
+ boxen(
614
+ chalk.green.bold("Setup complete") +
615
+ `\n\nConfig: ${chalk.cyan("emily.config.json")}` +
616
+ `\nOutput: ${chalk.cyan("dist/emily.min.css")}` +
617
+ `\nProject: ${chalk.cyan(detectedProject.name)}` +
618
+ `\nScan: ${chalk.cyan(config.purge.sourceDir)}` +
619
+ `\n\nNext: add ${chalk.yellow("dist/emily.min.css")} to your project.` +
620
+ (scriptsAdded
621
+ ? `\n\nScripts:\n${chalk.cyan("npm run emily:build")}\n${chalk.cyan("npm run emily:watch")}`
622
+ : ""),
623
+ {
624
+ padding: 1,
625
+ margin: 1,
626
+ borderStyle: "round",
627
+ borderColor: "magenta",
628
+ },
629
+ ),
630
+ );
246
631
  } else {
247
- console.log('\n❌ Build failed');
632
+ buildSpinner.fail("Automatic build failed.");
633
+
634
+ console.log("\nYour config was created, but CSS was not built.");
635
+ console.log("\nRun this manually:\n");
636
+ console.log(chalk.cyan(" npx emily-css build"));
637
+
638
+ if (stderr.trim()) {
639
+ console.log(chalk.gray("\nBuild error:\n"));
640
+ console.log(stderr.trim());
641
+ }
248
642
  }
643
+
644
+ process.exit(code === 0 ? 0 : 1);
645
+ });
646
+
647
+ build.on("error", (error) => {
648
+ buildSpinner.fail("Automatic build failed.");
649
+
650
+ console.log("\nYour config was created, but CSS was not built.");
651
+ console.log(`Reason: ${error.message}`);
652
+ console.log("\nRun this manually:\n");
653
+ console.log(chalk.cyan(" npx emily-css build\n"));
654
+
655
+ process.exit(1);
249
656
  });
657
+ } catch (error) {
658
+ console.log(chalk.red("\nSetup cancelled or failed."));
659
+
660
+ if (error && error.message) {
661
+ console.log(chalk.gray(error.message));
662
+ }
250
663
 
251
- } catch (err) {
252
- console.log(`\n❌ Error: ${err.message}`);
253
- rl.close();
664
+ process.exit(1);
254
665
  }
255
666
  }
256
667
 
package/src/purge-cmd.js CHANGED
@@ -30,7 +30,7 @@ function runPurge() {
30
30
  console.log('\nPurging unused utilities from ' + sourceDir + '...');
31
31
 
32
32
  const css = fs.readFileSync(cssPath, 'utf8');
33
- const purged = purgeCSS(css, sourceDir);
33
+ const purged = purgeCSS(css, sourceDir, config);
34
34
  const minified = purged
35
35
  .replace(/\/\*[\s\S]*?\*\//g, '')
36
36
  .replace(/\s+/g, ' ')
@@ -52,4 +52,4 @@ function runPurge() {
52
52
  console.log('\n ' + Math.round(original / 1024) + 'KB -> ' + Math.round(purgedSize / 1024) + 'KB (' + reduction + '% reduction)\n');
53
53
  }
54
54
 
55
- runPurge();
55
+ runPurge();
package/src/watch.js ADDED
@@ -0,0 +1,193 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chokidar = require('chokidar');
4
+ const chalk = require('chalk');
5
+ const { buildFullFramework, buildProductionCss, ensureFullFramework } = require('./index.js');
6
+ const { getAllFiles, extractClassNames } = require('./purge.js');
7
+
8
+ let isRunning = false;
9
+ let pendingRun = false;
10
+ let previousClasses = new Set();
11
+ let hasRunOnce = false;
12
+
13
+ function readConfig() {
14
+ const configPath = path.join(process.cwd(), 'emily.config.json');
15
+
16
+ if (!fs.existsSync(configPath)) {
17
+ console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
18
+ process.exit(1);
19
+ }
20
+
21
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
+ }
23
+
24
+ function shouldIgnore(filePath) {
25
+ const normalised = filePath.replace(/\\/g, '/');
26
+
27
+ return [
28
+ 'node_modules/',
29
+ '.git/',
30
+ '.nuxt/',
31
+ '.next/',
32
+ '.output/',
33
+ 'dist/',
34
+ 'build/',
35
+ 'coverage/',
36
+ '.cache/',
37
+ '.vite/'
38
+ ].some(part => normalised.includes('/' + part) || normalised.startsWith(part));
39
+ }
40
+
41
+ function runQuietly(fn) {
42
+ const originalLog = console.log;
43
+ const originalWarn = console.warn;
44
+
45
+ console.log = () => {};
46
+ console.warn = () => {};
47
+
48
+ try {
49
+ return fn();
50
+ } finally {
51
+ console.log = originalLog;
52
+ console.warn = originalWarn;
53
+ }
54
+ }
55
+
56
+ function collectUsedClasses(sourceDir, config) {
57
+ const files = getAllFiles(sourceDir, config.purge?.extensions);
58
+ const usedClasses = new Set();
59
+
60
+ for (const file of files) {
61
+ if (shouldIgnore(file)) continue;
62
+
63
+ try {
64
+ const content = fs.readFileSync(file, 'utf8');
65
+ extractClassNames(content).forEach(cls => usedClasses.add(cls));
66
+ } catch {}
67
+ }
68
+
69
+ return usedClasses;
70
+ }
71
+
72
+ function getClassDiff(currentClasses) {
73
+ const added = [...currentClasses].filter(cls => !previousClasses.has(cls));
74
+ const removed = [...previousClasses].filter(cls => !currentClasses.has(cls));
75
+
76
+ previousClasses = new Set(currentClasses);
77
+
78
+ return { added, removed };
79
+ }
80
+
81
+ function formatClassList(classes) {
82
+ if (classes.length === 0) return '';
83
+
84
+ const shown = classes.slice(0, 8).join(', ');
85
+ const extra = classes.length > 8 ? ' +' + (classes.length - 8) + ' more' : '';
86
+
87
+ return shown + extra;
88
+ }
89
+
90
+ function printSummary({ currentClasses, result, added, removed }) {
91
+ const reduction = (((result.originalSize - result.outputSize) / result.originalSize) * 100).toFixed(1);
92
+ const sizeKb = (result.outputSize / 1024).toFixed(1);
93
+ const time = new Date().toLocaleTimeString();
94
+
95
+ console.log(
96
+ chalk.green('✓ ' + time + ' updated') +
97
+ chalk.gray(' | ' + currentClasses.size + ' classes | ' + sizeKb + ' KB | ' + reduction + '% reduced')
98
+ );
99
+
100
+ if (!hasRunOnce) return;
101
+
102
+ if (removed.length > 0) {
103
+ console.log(chalk.red('− removed ' + removed.length + ' class' + (removed.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(removed) + ')'));
104
+ }
105
+
106
+ if (added.length > 0) {
107
+ console.log(chalk.green('+ added ' + added.length + ' class' + (added.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(added) + ')'));
108
+ }
109
+ }
110
+
111
+ function runProductionUpdate(filePath) {
112
+ if (isRunning) {
113
+ pendingRun = true;
114
+ return;
115
+ }
116
+
117
+ isRunning = true;
118
+
119
+ try {
120
+ const config = readConfig();
121
+ const sourceDir = config.purge?.sourceDir || '.';
122
+ const isConfigChange = filePath && filePath.replace(/\\/g, '/').endsWith('emily.config.json');
123
+ const cssPath = path.join(process.cwd(), 'dist/emily.css');
124
+
125
+ if (isConfigChange) {
126
+ runQuietly(() => buildFullFramework());
127
+ } else {
128
+ runQuietly(() => ensureFullFramework());
129
+ }
130
+
131
+ const result = runQuietly(() => buildProductionCss());
132
+ const currentClasses = collectUsedClasses(sourceDir, config);
133
+ const { added, removed } = getClassDiff(currentClasses);
134
+
135
+ printSummary({ currentClasses, result, added, removed });
136
+
137
+ hasRunOnce = true;
138
+ } catch (error) {
139
+ console.error('\n❌ EmilyUI watch failed');
140
+ console.error(error.message);
141
+ } finally {
142
+ isRunning = false;
143
+
144
+ if (pendingRun) {
145
+ pendingRun = false;
146
+ runProductionUpdate();
147
+ }
148
+ }
149
+ }
150
+
151
+ function getWatchPaths(config) {
152
+ return [
153
+ config.purge?.sourceDir || '.',
154
+ 'emily.config.json'
155
+ ];
156
+ }
157
+
158
+ function queueUpdate(filePath) {
159
+ if (filePath && shouldIgnore(filePath)) return;
160
+ runProductionUpdate(filePath);
161
+ }
162
+
163
+ function runWatch() {
164
+ const config = readConfig();
165
+ const watchPaths = getWatchPaths(config);
166
+
167
+ console.log('\n👀 EmilyUI is watching...');
168
+ console.log(chalk.gray(' Watching:'));
169
+ watchPaths.forEach(item => console.log(chalk.gray(' - ' + item)));
170
+
171
+ runQuietly(() => ensureFullFramework());
172
+ runProductionUpdate();
173
+
174
+ const watcher = chokidar.watch(watchPaths, {
175
+ ignored: shouldIgnore,
176
+ ignoreInitial: true,
177
+ awaitWriteFinish: {
178
+ stabilityThreshold: 500,
179
+ pollInterval: 100
180
+ }
181
+ });
182
+
183
+ watcher.on('change', queueUpdate);
184
+ watcher.on('add', queueUpdate);
185
+ watcher.on('unlink', queueUpdate);
186
+
187
+ watcher.on('error', error => {
188
+ console.error('\n❌ EmilyUI watcher error');
189
+ console.error(error.message);
190
+ });
191
+ }
192
+
193
+ runWatch();