emily-css 1.0.8 → 1.0.14

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/src/init.js CHANGED
@@ -1,256 +1,709 @@
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 { Select, Input, Confirm } = 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 COLOUR_PRESETS = {
27
+ primary: [
28
+ { value: "custom", label: "Enter your own hex" },
29
+ { value: "#DB2777", label: "Emily Pink" },
30
+ { value: "#2563EB", label: "Blue" },
31
+ { value: "#028090", label: "Teal" },
32
+ { value: "#114B5F", label: "Deep Teal" },
33
+ { value: "#15803D", label: "Green" },
34
+ { value: "#7C3AED", label: "Purple" },
35
+ { value: "#E05C00", label: "Burnt Orange" },
36
+ ],
37
+ secondary: [
38
+ { value: "custom", label: "Enter your own hex" },
39
+ { value: "#2563EB", label: "Blue" },
40
+ { value: "#028090", label: "Teal" },
41
+ { value: "#7C3AED", label: "Purple" },
42
+ { value: "#DB2777", label: "Emily Pink" },
43
+ { value: "#F59E0B", label: "Amber" },
44
+ { value: "#57534E", label: "Warm Grey" },
45
+ ],
46
+ success: [
47
+ { value: "#017F65", label: "Accessible Green (recommended)" },
48
+ { value: "#15803D", label: "Forest Green" },
49
+ { value: "custom", label: "Enter your own hex" },
50
+ ],
51
+ warning: [
52
+ { value: "#FFC107", label: "Amber (recommended)" },
53
+ { value: "#F59E0B", label: "Orange Amber" },
54
+ { value: "custom", label: "Enter your own hex" },
55
+ ],
56
+ error: [
57
+ { value: "#B20000", label: "Accessible Red (recommended)" },
58
+ { value: "#DC2626", label: "Red" },
59
+ { value: "custom", label: "Enter your own hex" },
60
+ ],
61
+ };
62
+
63
+ const FONT_OPTIONS = [
64
+ { name: "lexend", message: "Lexend (clear, accessible - recommended)" },
65
+ { name: "inter", message: "Inter (clean, widely used)" },
66
+ { name: "dm-sans", message: "DM Sans (modern, geometric)" },
67
+ { name: "nunito", message: "Nunito (friendly, rounded)" },
68
+ { name: "atkinson", message: "Atkinson Hyperlegible (maximum legibility)" },
69
+ { name: "system", message: "System sans-serif (no download required)" },
70
+ ];
71
+
72
+ const PURGE_EXTENSIONS = [
73
+ ".html",
74
+ ".htm",
75
+ ".twig",
76
+ ".njk",
77
+ ".liquid",
78
+ ".hbs",
79
+ ".jsx",
80
+ ".tsx",
81
+ ".vue",
82
+ ".php",
83
+ ".astro",
84
+ ".svelte",
85
+ ".blade.php",
86
+ ".jinja",
87
+ ".jinja2",
88
+ ".j2",
89
+ ".md",
90
+ ];
91
+
92
+ // ============================================================================
93
+ // HELPERS
94
+ // ============================================================================
18
95
 
19
- // Validate hex colour
20
96
  function isValidHex(hex) {
21
97
  return /^#[0-9A-F]{6}$/i.test(hex);
22
98
  }
23
99
 
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
- };
100
+ function colourSwatch(hex) {
101
+ return chalk.hex(hex)("■");
102
+ }
103
+
104
+ async function askHex(promptName, message, initial) {
105
+ const value = await new Input({
106
+ name: promptName,
107
+ message,
108
+ initial: initial || "#000000",
109
+ validate(value) {
110
+ return isValidHex(value) ? true : "Enter a valid hex colour, e.g. #0077B6";
111
+ },
112
+ }).run();
113
+ return value.toUpperCase();
114
+ }
33
115
 
34
- // Default config template - matches emily.config.json structure
35
- function createDefaultConfig(name, colours, fonts, baseUnit, sourceDir) {
116
+ async function askColourFromPresets(label, presets, defaultHex) {
117
+ const choices = presets.map(function(opt) {
118
+ if (opt.value === "custom") {
119
+ return { name: "custom", message: "Enter your own hex" };
120
+ }
121
+ return {
122
+ name: opt.value,
123
+ message: colourSwatch(opt.value) + " " + opt.label + " " + chalk.gray(opt.value),
124
+ };
125
+ });
126
+
127
+ const selected = await new Select({
128
+ name: label,
129
+ message: label + " colour",
130
+ choices: choices,
131
+ }).run();
132
+
133
+ if (selected !== "custom") return selected.toUpperCase();
134
+ return askHex(label + "Custom", "Enter " + label + " hex", defaultHex);
135
+ }
136
+
137
+ async function askBtnColour(label, matchLabel, matchHex, presets) {
138
+ const sameChoice = {
139
+ name: matchHex,
140
+ message: colourSwatch(matchHex) + " Same as " + matchLabel + " " + chalk.gray(matchHex),
141
+ };
142
+
143
+ const otherChoices = presets
144
+ .filter(function(opt) { return opt.value !== matchHex; })
145
+ .map(function(opt) {
146
+ if (opt.value === "custom") {
147
+ return { name: "custom", message: "Enter your own hex" };
148
+ }
149
+ return {
150
+ name: opt.value,
151
+ message: colourSwatch(opt.value) + " " + opt.label + " " + chalk.gray(opt.value),
152
+ };
153
+ });
154
+
155
+ const selected = await new Select({
156
+ name: label,
157
+ message: label + " colour",
158
+ choices: [sameChoice].concat(otherChoices),
159
+ }).run();
160
+
161
+ if (selected !== "custom") return selected.toUpperCase();
162
+ return askHex(label + "Custom", "Enter " + label + " hex", matchHex);
163
+ }
164
+
165
+ function hasFile(fileName) {
166
+ return fs.existsSync(path.join(process.cwd(), fileName));
167
+ }
168
+
169
+ function readPackageJson() {
170
+ const packagePath = path.join(process.cwd(), "package.json");
171
+ if (!fs.existsSync(packagePath)) return null;
172
+ try {
173
+ return JSON.parse(fs.readFileSync(packagePath, "utf8"));
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ function hasDependency(packageJson, dependencyName) {
180
+ if (!packageJson) return false;
181
+ return Boolean(
182
+ packageJson.dependencies?.[dependencyName] ||
183
+ packageJson.devDependencies?.[dependencyName],
184
+ );
185
+ }
186
+
187
+ function addEmilyScriptsToPackageJson() {
188
+ const packagePath = path.join(process.cwd(), "package.json");
189
+ if (!fs.existsSync(packagePath)) return false;
190
+ try {
191
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
192
+ packageJson.scripts = packageJson.scripts || {};
193
+ let changed = false;
194
+ const scripts = {
195
+ "emily:build": "emily-css build",
196
+ "emily:watch": "emily-css watch",
197
+ "emily:help": "emily-css help",
198
+ "emily:showcase": "emily-css showcase",
199
+ };
200
+ for (const [key, val] of Object.entries(scripts)) {
201
+ if (!packageJson.scripts[key]) {
202
+ packageJson.scripts[key] = val;
203
+ changed = true;
204
+ }
205
+ }
206
+ if (changed) {
207
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + "\n");
208
+ }
209
+ return true;
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ // ============================================================================
216
+ // PROJECT DETECTION
217
+ // ============================================================================
218
+
219
+ function detectProject() {
220
+ const packageJson = readPackageJson();
221
+
222
+ if (
223
+ hasFile("nuxt.config.ts") ||
224
+ hasFile("nuxt.config.js") ||
225
+ hasDependency(packageJson, "nuxt")
226
+ ) {
227
+ return {
228
+ name: "Nuxt",
229
+ sourceDir: ".",
230
+ sourceGlobs: [
231
+ "./components/**/*.{vue,js,ts}",
232
+ "./pages/**/*.vue",
233
+ "./layouts/**/*.vue",
234
+ "./app.vue",
235
+ ],
236
+ };
237
+ }
238
+
239
+ if (hasDependency(packageJson, "next")) {
240
+ return {
241
+ name: "Next.js",
242
+ sourceDir: ".",
243
+ sourceGlobs: [
244
+ "./app/**/*.{js,jsx,ts,tsx}",
245
+ "./pages/**/*.{js,jsx,ts,tsx}",
246
+ "./components/**/*.{js,jsx,ts,tsx}",
247
+ "./src/**/*.{js,jsx,ts,tsx}",
248
+ ],
249
+ };
250
+ }
251
+
252
+ if (hasDependency(packageJson, "react")) {
253
+ return {
254
+ name: "React",
255
+ sourceDir: "./src",
256
+ sourceGlobs: [
257
+ "./src/**/*.{js,jsx,ts,tsx}",
258
+ "./components/**/*.{js,jsx,ts,tsx}",
259
+ ],
260
+ };
261
+ }
262
+
263
+ if (
264
+ hasDependency(packageJson, "vue") ||
265
+ hasFile("vite.config.ts") ||
266
+ hasFile("vite.config.js")
267
+ ) {
268
+ return {
269
+ name: "Vue/Vite",
270
+ sourceDir: "./src",
271
+ sourceGlobs: ["./src/**/*.{vue,js,ts}"],
272
+ };
273
+ }
274
+
275
+ if (hasDependency(packageJson, "astro") || hasFile("astro.config.mjs")) {
276
+ return {
277
+ name: "Astro",
278
+ sourceDir: "./src",
279
+ sourceGlobs: ["./src/**/*.{astro,html,js,ts,vue,jsx,tsx,svelte}"],
280
+ };
281
+ }
282
+
283
+ const rootFiles = fs.readdirSync(process.cwd());
284
+ const hasDrupalInfoFile = rootFiles.some((file) =>
285
+ file.endsWith(".info.yml"),
286
+ );
287
+
288
+ if (
289
+ hasDrupalInfoFile ||
290
+ fs.existsSync(path.join(process.cwd(), "web/core"))
291
+ ) {
292
+ return {
293
+ name: "Drupal",
294
+ sourceDir: ".",
295
+ sourceGlobs: [
296
+ "./web/themes/custom/**/*.{twig,js,ts}",
297
+ "./templates/**/*.html.twig",
298
+ "./components/**/*.twig",
299
+ "./**/*.theme",
300
+ ],
301
+ };
302
+ }
303
+
304
+ return {
305
+ name: "Static/Generic",
306
+ sourceDir: ".",
307
+ sourceGlobs: [
308
+ "./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,js,ts}",
309
+ ],
310
+ };
311
+ }
312
+
313
+ // ============================================================================
314
+ // CONFIG BUILDER
315
+ // ============================================================================
316
+
317
+ function createDefaultConfig({
318
+ name,
319
+ colours,
320
+ headingFont,
321
+ bodyFont,
322
+ baseUnit,
323
+ detectedProject,
324
+ sourceDir,
325
+ }) {
36
326
  return {
37
327
  name,
38
- description: `${name} design system`,
39
- baseUnit: `${baseUnit}px`,
40
- baseFontSize: '16px',
41
- fontFamily: { heading: 'lexend', body: 'inter' },
328
+ description: name + " design system",
329
+
330
+ baseUnit: baseUnit + "px",
331
+ baseFontSize: "16px",
332
+
333
+ fontFamily: {
334
+ heading: headingFont,
335
+ body: bodyFont,
336
+ },
337
+
42
338
  customFonts: [],
339
+
43
340
  colours,
341
+
342
+ purge: {
343
+ projectType: detectedProject.name,
344
+ sourceDir,
345
+ sourceGlobs: detectedProject.sourceGlobs,
346
+ ignore: DEFAULT_PURGE_IGNORE,
347
+ extensions: PURGE_EXTENSIONS,
348
+ },
349
+
44
350
  breakpoints: {
45
- sm: '640px',
46
- md: '768px',
47
- lg: '1024px',
48
- xl: '1280px',
49
- '2xl': '1536px'
351
+ sm: "640px",
352
+ md: "768px",
353
+ lg: "1024px",
354
+ xl: "1280px",
355
+ "2xl": "1536px",
50
356
  },
357
+
51
358
  spacing: {
52
359
  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'
360
+ 0: "0px",
361
+ px: "1px",
362
+ 0.5: "0.125rem",
363
+ 1: "0.25rem",
364
+ 1.5: "0.375rem",
365
+ 2: "0.5rem",
366
+ 2.5: "0.625rem",
367
+ 3: "0.75rem",
368
+ 3.5: "0.875rem",
369
+ 4: "1rem",
370
+ 5: "1.25rem",
371
+ 6: "1.5rem",
372
+ 7: "1.75rem",
373
+ 8: "2rem",
374
+ 9: "2.25rem",
375
+ 10: "2.5rem",
376
+ 11: "2.75rem",
377
+ 12: "3rem",
378
+ 14: "3.5rem",
379
+ 16: "4rem",
380
+ 20: "5rem",
381
+ 24: "6rem",
382
+ 28: "7rem",
383
+ 32: "8rem",
384
+ 36: "9rem",
385
+ 40: "10rem",
386
+ 44: "11rem",
387
+ 48: "12rem",
388
+ 52: "13rem",
389
+ 56: "14rem",
390
+ 60: "15rem",
391
+ 64: "16rem",
392
+ 72: "18rem",
393
+ 80: "20rem",
394
+ 96: "24rem",
88
395
  },
396
+
89
397
  borderWidths: [0, 2, 4, 8],
398
+
90
399
  borderRadius: {
91
- 'none': '0',
92
- 'sm': '4px',
93
- 'base': '8px',
94
- 'md': '12px',
95
- 'lg': '16px',
96
- 'full': '9999px'
97
- }
400
+ none: "0",
401
+ sm: "4px",
402
+ base: "8px",
403
+ md: "12px",
404
+ lg: "16px",
405
+ full: "9999px",
406
+ },
98
407
  },
408
+
99
409
  typography: {
100
410
  lineHeightRatio: 1.5,
411
+
101
412
  fontWeights: {
102
413
  light: 300,
103
414
  normal: 400,
104
415
  medium: 500,
105
416
  semibold: 600,
106
- bold: 700
417
+ bold: 700,
107
418
  },
419
+
108
420
  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
- ]
421
+ { name: "xs", value: "12px", lineHeight: 1.5 },
422
+ { name: "sm", value: "14px", lineHeight: 1.5 },
423
+ { name: "base", value: "16px", lineHeight: 1.6 },
424
+ { name: "lg", value: "18px", lineHeight: 1.6 },
425
+ { name: "xl", value: "20px", lineHeight: 1.6 },
426
+ { name: "2xl", value: "24px", lineHeight: 1.4 },
427
+ { name: "3xl", value: "30px", lineHeight: 1.4 },
428
+ { name: "4xl", value: "36px", lineHeight: 1.3 },
429
+ ],
118
430
  },
431
+
119
432
  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'
433
+ sm: "0 1px 2px rgba(0, 0, 0, 0.05)",
434
+ base: "0 4px 6px rgba(0, 0, 0, 0.1)",
435
+ md: "0 10px 15px rgba(0, 0, 0, 0.1)",
436
+ lg: "0 20px 25px rgba(0, 0, 0, 0.15)",
437
+ none: "none",
125
438
  },
439
+
126
440
  transitions: {
127
- fast: '100ms',
128
- base: '200ms',
129
- slow: '300ms',
130
- timing: 'cubic-bezier(0.4, 0, 0.2, 1)'
441
+ fast: "100ms",
442
+ base: "200ms",
443
+ slow: "300ms",
444
+ timing: "cubic-bezier(0.4, 0, 0.2, 1)",
131
445
  },
446
+
132
447
  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'
448
+ auto: "auto",
449
+ 0: "0",
450
+ 10: "10",
451
+ 20: "20",
452
+ 30: "30",
453
+ 40: "40",
454
+ 50: "50",
455
+ dropdown: "1000",
456
+ sticky: "1020",
457
+ fixed: "1030",
458
+ modal: "1040",
459
+ popover: "1060",
460
+ tooltip: "1070",
146
461
  },
147
- opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100]
462
+
463
+ opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100],
148
464
  };
149
465
  }
150
466
 
467
+ // ============================================================================
468
+ // INIT
469
+ // ============================================================================
470
+
151
471
  async function init() {
152
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
153
- console.log(' EmilyUI Setup');
154
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
472
+ console.log(chalk.bold.magenta("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
473
+ console.log(chalk.bold.magenta(" EmilyUI Setup"));
474
+ console.log(chalk.bold.magenta("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
155
475
 
156
476
  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;
163
- }
477
+ const spinner = ora("Analysing project structure...").start();
478
+ const detectedProject = detectProject();
479
+ spinner.succeed("Detected project: " + chalk.cyan(detectedProject.name));
164
480
 
165
- // 2. Brand colours
166
- console.log('\nBrand colours (hex format, e.g., #0077b6):');
481
+ // Derive a sensible default name from package.json if available
482
+ const packageJsonData = readPackageJson();
483
+ const pkgName = packageJsonData && packageJsonData.name
484
+ ? packageJsonData.name.replace(/-/g, " ").replace(/\b\w/g, function(c) { return c.toUpperCase(); })
485
+ : "My Design System";
167
486
 
168
- const colours = {};
169
- const colourNames = ['primary', 'secondary', 'success', 'warning', 'error', 'neutral'];
487
+ const projectName = await new Input({
488
+ name: "projectName",
489
+ message: "Project name",
490
+ initial: pkgName,
491
+ validate: function(v) { return v.trim() ? true : "Project name is required"; },
492
+ }).run();
170
493
 
171
- for (const colourName of colourNames) {
172
- let colour;
173
- let valid = false;
494
+ if (!projectName || !projectName.trim()) {
495
+ console.log(chalk.red("\nProject name is required.\n"));
496
+ process.exit(1);
497
+ }
174
498
 
175
- while (!valid) {
176
- colour = await prompt(` ${colourName} [${defaultColours[colourName]}]: `);
177
- colour = colour || defaultColours[colourName];
499
+ // =========================================================================
500
+ // COLOURS
501
+ // =========================================================================
178
502
 
179
- if (isValidHex(colour)) {
180
- colours[colourName] = colour;
181
- valid = true;
182
- } else {
183
- console.log(` Invalid hex colour. Use format: #0077b6`);
184
- }
185
- }
186
- }
503
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Brand colours"));
504
+
505
+ const primary = await askColourFromPresets("primary", COLOUR_PRESETS.primary, "#DB2777");
506
+ const secondary = await askColourFromPresets("secondary", COLOUR_PRESETS.secondary, "#2563EB");
507
+ const btnPrimary = await askBtnColour("btn-primary", "primary", primary, COLOUR_PRESETS.primary);
508
+ const btnSecondary = await askBtnColour("btn-secondary", "secondary", secondary, COLOUR_PRESETS.secondary);
509
+
510
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Utility colours"));
511
+ console.log(chalk.gray(" Defaults shown. Press enter to accept or pick an alternative.\n"));
187
512
 
188
- // 3. Fonts
189
- console.log('\nFont families (optional, press Enter to skip):');
513
+ const success = await askColourFromPresets("success", COLOUR_PRESETS.success, "#017F65");
514
+ const warning = await askColourFromPresets("warning", COLOUR_PRESETS.warning, "#FFC107");
515
+ const error = await askColourFromPresets("error", COLOUR_PRESETS.error, "#B20000");
190
516
 
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'
517
+ const colours = {
518
+ primary: primary,
519
+ secondary: secondary,
520
+ "btn-primary": btnPrimary,
521
+ "btn-secondary": btnSecondary,
522
+ success: success,
523
+ warning: warning,
524
+ error: error,
525
+ neutral: "#57534E",
195
526
  };
196
527
 
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');
528
+ // Additional utility colours
529
+ let addingMore = true;
530
+ while (addingMore) {
531
+ const wantsMore = await new Confirm({
532
+ name: "addMore",
533
+ message: "Add another utility colour?",
534
+ initial: false,
535
+ }).run();
536
+
537
+ if (!wantsMore) {
538
+ addingMore = false;
539
+ break;
206
540
  }
541
+
542
+ const customName = await new Input({
543
+ name: "customName",
544
+ message: "Colour name (e.g. accent, highlight, brand-dark)",
545
+ validate: function(value) {
546
+ const trimmed = value.trim();
547
+ if (!trimmed) return "Name is required";
548
+ if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) return "Use lowercase letters, numbers, and hyphens only";
549
+ if (colours[trimmed]) return '"' + trimmed + '" is already defined';
550
+ return true;
551
+ },
552
+ }).run();
553
+
554
+ colours[customName.trim()] = await askHex("hex-" + customName, "Hex for " + customName, "#000000");
207
555
  }
208
556
 
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';
557
+ // =========================================================================
558
+ // TYPOGRAPHY
559
+ // =========================================================================
213
560
 
214
- // 6. Create config
215
- const config = createDefaultConfig(name, colours, fonts, baseUnit, sourceDir);
561
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Typography"));
216
562
 
217
- // 7. Write config file to the user's project directory
218
- const configPath = path.join(process.cwd(), 'emily.config.json');
219
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
563
+ const headingFont = await new Select({
564
+ name: "headingFont",
565
+ message: "Heading font",
566
+ choices: FONT_OPTIONS,
567
+ initial: 0,
568
+ }).run();
569
+
570
+ const bodyFont = await new Select({
571
+ name: "bodyFont",
572
+ message: "Body font",
573
+ choices: FONT_OPTIONS,
574
+ initial: 1,
575
+ }).run();
576
+
577
+ // =========================================================================
578
+ // SPACING
579
+ // =========================================================================
580
+
581
+ const baseUnitRaw = await new Input({
582
+ name: "baseUnit",
583
+ message: "Base spacing unit in px (18px = 1.125rem)",
584
+ initial: "18",
585
+ validate: function(value) {
586
+ const parsed = Number.parseInt(value, 10);
587
+ if (Number.isNaN(parsed) || parsed <= 0) return "Must be a positive number.";
588
+ return true;
589
+ },
590
+ }).run();
591
+
592
+ const baseUnit = Number.parseInt(baseUnitRaw, 10);
593
+
594
+ // =========================================================================
595
+ // PURGE
596
+ // =========================================================================
597
+
598
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Purge settings"));
220
599
 
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}`);
600
+ const sourceDirRaw = await new Input({
601
+ name: "sourceDir",
602
+ message: "Detected " + detectedProject.name + " — scan directory",
603
+ initial: detectedProject.sourceDir,
604
+ }).run();
226
605
 
227
- // 8. Run build
228
- console.log('\n🔨 Building CSS...\n');
229
- rl.close();
606
+ // =========================================================================
607
+ // BUILD
608
+ // =========================================================================
230
609
 
231
- // Spawn build process
232
- const { spawn } = require('child_process');
233
- const build = spawn('npx', ['emily-css', 'build'], {
610
+ const config = createDefaultConfig({
611
+ name: projectName.trim(),
612
+ colours: colours,
613
+ headingFont: headingFont,
614
+ bodyFont: bodyFont,
615
+ baseUnit: baseUnit,
616
+ detectedProject: detectedProject,
617
+ sourceDir: sourceDirRaw.trim() || detectedProject.sourceDir,
618
+ });
619
+
620
+ const configPath = path.join(process.cwd(), "emily.config.json");
621
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
622
+
623
+ console.log("");
624
+ const buildSpinner = ora("Building EmilyUI CSS...").start();
625
+
626
+ const build = crossSpawn("npx", ["emily-css", "build"], {
234
627
  cwd: process.cwd(),
235
- stdio: 'inherit'
628
+ stdio: "pipe",
629
+ shell: process.platform === "win32",
236
630
  });
237
631
 
238
- build.on('close', code => {
632
+ let stderr = "";
633
+ build.stderr.on("data", function(data) { stderr += data.toString(); });
634
+
635
+ build.on("close", async function(code) {
239
636
  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)');
637
+ buildSpinner.succeed("EmilyUI CSS built successfully.");
638
+
639
+ const scriptsAdded = addEmilyScriptsToPackageJson();
640
+
641
+ console.log(
642
+ "\n" +
643
+ boxen(
644
+ chalk.green.bold("Setup complete") +
645
+ "\n\nConfig: " + chalk.cyan("emily.config.json") +
646
+ "\nOutput: " + chalk.cyan("dist/emily.min.css") +
647
+ "\nProject: " + chalk.cyan(detectedProject.name) +
648
+ "\nScan: " + chalk.cyan(config.purge.sourceDir) +
649
+ "\n\nNext: link " + chalk.yellow("dist/emily.min.css") + " in your project." +
650
+ (scriptsAdded
651
+ ? "\n\nScripts added:\n" +
652
+ chalk.cyan(" npm run emily:build\n") +
653
+ chalk.cyan(" npm run emily:watch\n") +
654
+ chalk.cyan(" npm run emily:showcase\n") +
655
+ chalk.cyan(" npm run emily:help")
656
+ : ""),
657
+ { padding: 1, margin: 1, borderStyle: "round", borderColor: "magenta" },
658
+ ),
659
+ );
660
+
661
+ const startWatch = await new Confirm({
662
+ name: "startWatch",
663
+ message: "Start the file watcher now?",
664
+ initial: true,
665
+ }).run();
666
+
667
+ if (startWatch) {
668
+ console.log(chalk.cyan("\nStarting watcher — press Ctrl+C to stop.\n"));
669
+ const watcher = crossSpawn("npx", ["emily-css", "watch"], {
670
+ cwd: process.cwd(),
671
+ stdio: "inherit",
672
+ shell: process.platform === "win32",
673
+ });
674
+ watcher.on("close", function(c) { process.exit(c || 0); });
675
+ } else {
676
+ console.log(chalk.gray("\nRun the watcher any time with: npm run emily:watch\n"));
677
+ process.exit(0);
678
+ }
246
679
  } else {
247
- console.log('\n❌ Build failed');
680
+ buildSpinner.fail("Automatic build failed.");
681
+ console.log("\nYour config was created, but CSS was not built.");
682
+ console.log("\nRun manually:\n");
683
+ console.log(chalk.cyan(" npx emily-css build"));
684
+ if (stderr.trim()) {
685
+ console.log(chalk.gray("\nBuild error:\n"));
686
+ console.log(stderr.trim());
687
+ }
688
+ process.exit(1);
248
689
  }
249
690
  });
250
691
 
251
- } catch (err) {
252
- console.log(`\n❌ Error: ${err.message}`);
253
- rl.close();
692
+ build.on("error", function(error) {
693
+ buildSpinner.fail("Automatic build failed.");
694
+ console.log("\nYour config was created, but CSS was not built.");
695
+ console.log("Reason: " + error.message);
696
+ console.log("\nRun manually:\n");
697
+ console.log(chalk.cyan(" npx emily-css build\n"));
698
+ process.exit(1);
699
+ });
700
+
701
+ } catch (error) {
702
+ console.log(chalk.red("\nSetup cancelled or failed."));
703
+ if (error && error.message) {
704
+ console.log(chalk.gray(error.message));
705
+ }
706
+ process.exit(1);
254
707
  }
255
708
  }
256
709