emily-css 1.0.9 → 1.0.15

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,7 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const crossSpawn = require("cross-spawn");
4
- const { Form, Select, Input } = require("enquirer");
4
+ const { Select, Input, Confirm } = require("enquirer");
5
5
  const chalk = require("chalk");
6
6
  const ora = require("ora");
7
7
  const boxen = require("boxen");
@@ -23,62 +23,50 @@ const DEFAULT_PURGE_IGNORE = [
23
23
  ".vite",
24
24
  ];
25
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
26
  const COLOUR_PRESETS = {
36
27
  primary: [
28
+ { value: "custom", label: "Enter your own hex" },
37
29
  { value: "#DB2777", label: "Emily Pink" },
38
- { value: "#114B5F", label: "Deep Teal" },
39
30
  { value: "#2563EB", label: "Blue" },
40
- { value: "#017F65", label: "Green" },
41
- { value: "custom", label: "Custom hex" },
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" },
42
36
  ],
43
37
  secondary: [
38
+ { value: "custom", label: "Enter your own hex" },
44
39
  { value: "#2563EB", label: "Blue" },
45
40
  { value: "#028090", label: "Teal" },
46
41
  { value: "#7C3AED", label: "Purple" },
47
42
  { value: "#DB2777", label: "Emily Pink" },
48
- { value: "custom", label: "Custom hex" },
43
+ { value: "#F59E0B", label: "Amber" },
44
+ { value: "#57534E", label: "Warm Grey" },
49
45
  ],
50
46
  success: [
51
- { value: "#017F65", label: "Accessible Green" },
47
+ { value: "#017F65", label: "Accessible Green (recommended)" },
52
48
  { value: "#15803D", label: "Forest Green" },
53
- { value: "#50C878", label: "Emerald" },
54
- { value: "custom", label: "Custom hex" },
49
+ { value: "custom", label: "Enter your own hex" },
55
50
  ],
56
51
  warning: [
57
- { value: "#FFC107", label: "Amber" },
58
- { value: "#F59E0B", label: "Orange" },
59
- { value: "#FFBF00", label: "Yellow" },
60
- { value: "custom", label: "Custom hex" },
52
+ { value: "#FFC107", label: "Amber (recommended)" },
53
+ { value: "#F59E0B", label: "Orange Amber" },
54
+ { value: "custom", label: "Enter your own hex" },
61
55
  ],
62
56
  error: [
63
- { value: "#B20000", label: "Accessible Red" },
57
+ { value: "#B20000", label: "Accessible Red (recommended)" },
64
58
  { 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" },
59
+ { value: "custom", label: "Enter your own hex" },
73
60
  ],
74
61
  };
75
62
 
76
63
  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" },
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)" },
82
70
  ];
83
71
 
84
72
  const PURGE_EXTENSIONS = [
@@ -109,47 +97,69 @@ function isValidHex(hex) {
109
97
  return /^#[0-9A-F]{6}$/i.test(hex);
110
98
  }
111
99
 
112
- function colourChoice(hex, label) {
113
- if (hex === "custom") {
114
- return {
115
- name: "custom",
116
- message: "Custom hex",
117
- };
118
- }
100
+ function colourSwatch(hex) {
101
+ return chalk.hex(hex)("");
102
+ }
119
103
 
120
- return {
121
- name: hex,
122
- message: `${chalk.hex(hex)("■")} ${label} ${chalk.gray(hex)}`,
123
- };
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();
124
114
  }
125
115
 
126
- async function askColour(colourName) {
127
- const choices = COLOUR_PRESETS[colourName].map((option) =>
128
- colourChoice(option.value, option.label),
129
- );
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
+ });
130
126
 
131
127
  const selected = await new Select({
132
- name: colourName,
133
- message: `${colourName} colour`,
134
- choices,
128
+ name: label,
129
+ message: label + " colour",
130
+ choices: choices,
135
131
  }).run();
136
132
 
137
- if (selected !== "custom") {
138
- return selected.toUpperCase();
139
- }
133
+ if (selected !== "custom") return selected.toUpperCase();
134
+ return askHex(label + "Custom", "Enter " + label + " hex", defaultHex);
135
+ }
140
136
 
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
- },
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),
150
159
  }).run();
151
160
 
152
- return custom.toUpperCase();
161
+ if (selected !== "custom") return selected.toUpperCase();
162
+ return askHex(label + "Custom", "Enter " + label + " hex", matchHex);
153
163
  }
154
164
 
155
165
  function hasFile(fileName) {
@@ -158,11 +168,7 @@ function hasFile(fileName) {
158
168
 
159
169
  function readPackageJson() {
160
170
  const packagePath = path.join(process.cwd(), "package.json");
161
-
162
- if (!fs.existsSync(packagePath)) {
163
- return null;
164
- }
165
-
171
+ if (!fs.existsSync(packagePath)) return null;
166
172
  try {
167
173
  return JSON.parse(fs.readFileSync(packagePath, "utf8"));
168
174
  } catch {
@@ -172,7 +178,6 @@ function readPackageJson() {
172
178
 
173
179
  function hasDependency(packageJson, dependencyName) {
174
180
  if (!packageJson) return false;
175
-
176
181
  return Boolean(
177
182
  packageJson.dependencies?.[dependencyName] ||
178
183
  packageJson.devDependencies?.[dependencyName],
@@ -181,35 +186,26 @@ function hasDependency(packageJson, dependencyName) {
181
186
 
182
187
  function addEmilyScriptsToPackageJson() {
183
188
  const packagePath = path.join(process.cwd(), "package.json");
184
-
185
- if (!fs.existsSync(packagePath)) {
186
- return false;
187
- }
188
-
189
+ if (!fs.existsSync(packagePath)) return false;
189
190
  try {
190
191
  const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
191
-
192
192
  packageJson.scripts = packageJson.scripts || {};
193
-
194
193
  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;
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
+ }
204
205
  }
205
-
206
206
  if (changed) {
207
- fs.writeFileSync(
208
- packagePath,
209
- JSON.stringify(packageJson, null, 2) + "\n",
210
- );
207
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + "\n");
211
208
  }
212
-
213
209
  return true;
214
210
  } catch {
215
211
  return false;
@@ -323,22 +319,20 @@ function createDefaultConfig({
323
319
  colours,
324
320
  headingFont,
325
321
  bodyFont,
326
- monoFont,
327
322
  baseUnit,
328
323
  detectedProject,
329
324
  sourceDir,
330
325
  }) {
331
326
  return {
332
327
  name,
333
- description: `${name} design system`,
328
+ description: name + " design system",
334
329
 
335
- baseUnit: `${baseUnit}px`,
330
+ baseUnit: baseUnit + "px",
336
331
  baseFontSize: "16px",
337
332
 
338
333
  fontFamily: {
339
334
  heading: headingFont,
340
335
  body: bodyFont,
341
- mono: monoFont,
342
336
  },
343
337
 
344
338
  customFonts: [],
@@ -475,29 +469,26 @@ function createDefaultConfig({
475
469
  // ============================================================================
476
470
 
477
471
  async function init() {
478
- console.log(
479
- chalk.bold.magenta("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"),
480
- );
472
+ console.log(chalk.bold.magenta("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
481
473
  console.log(chalk.bold.magenta(" EmilyUI Setup"));
482
- console.log(
483
- chalk.bold.magenta("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"),
484
- );
474
+ console.log(chalk.bold.magenta("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
485
475
 
486
476
  try {
487
477
  const spinner = ora("Analysing project structure...").start();
488
478
  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
- ],
479
+ spinner.succeed("Detected project: " + chalk.cyan(detectedProject.name));
480
+
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";
486
+
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"; },
501
492
  }).run();
502
493
 
503
494
  if (!projectName || !projectName.trim()) {
@@ -505,15 +496,69 @@ async function init() {
505
496
  process.exit(1);
506
497
  }
507
498
 
508
- console.log(chalk.bold(`\n${chalk.magenta("→")} Brand colours`));
499
+ // =========================================================================
500
+ // COLOURS
501
+ // =========================================================================
502
+
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"));
512
+
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");
516
+
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",
526
+ };
509
527
 
510
- const normalisedColours = {};
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;
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();
511
553
 
512
- for (const colourName of Object.keys(DEFAULT_COLOURS)) {
513
- normalisedColours[colourName] = await askColour(colourName);
554
+ colours[customName.trim()] = await askHex("hex-" + customName, "Hex for " + customName, "#000000");
514
555
  }
515
556
 
516
- console.log(chalk.bold(`\n${chalk.magenta("→")} Typography`));
557
+ // =========================================================================
558
+ // TYPOGRAPHY
559
+ // =========================================================================
560
+
561
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Typography"));
517
562
 
518
563
  const headingFont = await new Select({
519
564
  name: "headingFont",
@@ -529,59 +574,47 @@ async function init() {
529
574
  initial: 1,
530
575
  }).run();
531
576
 
532
- const monoFont = await new Select({
533
- name: "monoFont",
534
- message: "Monospace font",
535
- choices: FONT_OPTIONS,
536
- initial: 4,
537
- }).run();
538
-
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);
551
-
552
- if (Number.isNaN(parsed) || parsed <= 0) {
553
- return "Base spacing unit must be a positive number.";
554
- }
555
-
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.";
556
588
  return true;
557
589
  },
558
590
  }).run();
559
591
 
560
- const baseUnit = Number.parseInt(baseUnitInput, 10);
592
+ const baseUnit = Number.parseInt(baseUnitRaw, 10);
561
593
 
562
- console.log(chalk.bold(`\n${chalk.magenta("→")} Purge settings`));
594
+ // =========================================================================
595
+ // PURGE
596
+ // =========================================================================
563
597
 
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
- ],
598
+ console.log(chalk.bold("\n" + chalk.magenta("→") + " Purge settings"));
599
+
600
+ const sourceDirRaw = await new Input({
601
+ name: "sourceDir",
602
+ message: "Detected " + detectedProject.name + " — scan directory",
603
+ initial: detectedProject.sourceDir,
574
604
  }).run();
575
605
 
606
+ // =========================================================================
607
+ // BUILD
608
+ // =========================================================================
609
+
576
610
  const config = createDefaultConfig({
577
611
  name: projectName.trim(),
578
- colours: normalisedColours,
579
- headingFont,
580
- bodyFont,
581
- monoFont,
582
- baseUnit,
583
- detectedProject,
584
- sourceDir: sourceDir.trim() || detectedProject.sourceDir,
612
+ colours: colours,
613
+ headingFont: headingFont,
614
+ bodyFont: bodyFont,
615
+ baseUnit: baseUnit,
616
+ detectedProject: detectedProject,
617
+ sourceDir: sourceDirRaw.trim() || detectedProject.sourceDir,
585
618
  });
586
619
 
587
620
  const configPath = path.join(process.cwd(), "emily.config.json");
@@ -597,12 +630,9 @@ async function init() {
597
630
  });
598
631
 
599
632
  let stderr = "";
633
+ build.stderr.on("data", function(data) { stderr += data.toString(); });
600
634
 
601
- build.stderr.on("data", (data) => {
602
- stderr += data.toString();
603
- });
604
-
605
- build.on("close", (code) => {
635
+ build.on("close", async function(code) {
606
636
  if (code === 0) {
607
637
  buildSpinner.succeed("EmilyUI CSS built successfully.");
608
638
 
@@ -610,57 +640,69 @@ async function init() {
610
640
 
611
641
  console.log(
612
642
  "\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
- ),
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
+ ),
630
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
+ }
631
679
  } else {
632
680
  buildSpinner.fail("Automatic build failed.");
633
-
634
681
  console.log("\nYour config was created, but CSS was not built.");
635
- console.log("\nRun this manually:\n");
682
+ console.log("\nRun manually:\n");
636
683
  console.log(chalk.cyan(" npx emily-css build"));
637
-
638
684
  if (stderr.trim()) {
639
685
  console.log(chalk.gray("\nBuild error:\n"));
640
686
  console.log(stderr.trim());
641
687
  }
688
+ process.exit(1);
642
689
  }
643
-
644
- process.exit(code === 0 ? 0 : 1);
645
690
  });
646
691
 
647
- build.on("error", (error) => {
692
+ build.on("error", function(error) {
648
693
  buildSpinner.fail("Automatic build failed.");
649
-
650
694
  console.log("\nYour config was created, but CSS was not built.");
651
- console.log(`Reason: ${error.message}`);
652
- console.log("\nRun this manually:\n");
695
+ console.log("Reason: " + error.message);
696
+ console.log("\nRun manually:\n");
653
697
  console.log(chalk.cyan(" npx emily-css build\n"));
654
-
655
698
  process.exit(1);
656
699
  });
700
+
657
701
  } catch (error) {
658
702
  console.log(chalk.red("\nSetup cancelled or failed."));
659
-
660
703
  if (error && error.message) {
661
704
  console.log(chalk.gray(error.message));
662
705
  }
663
-
664
706
  process.exit(1);
665
707
  }
666
708
  }