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/CHANGELOG.md +154 -0
- package/README.md +64 -222
- package/bin/emilyui.js +35 -6
- package/package.json +18 -4
- package/src/generators.js +34 -1
- package/src/index.js +148 -47
- package/src/init.js +641 -188
- package/src/purge-cmd.js +2 -2
- package/src/showcase.js +90 -0
- package/src/watch.js +193 -0
package/src/init.js
CHANGED
|
@@ -1,256 +1,709 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
46
|
-
md:
|
|
47
|
-
lg:
|
|
48
|
-
xl:
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
110
|
-
{ name:
|
|
111
|
-
{ name:
|
|
112
|
-
{ name:
|
|
113
|
-
{ name:
|
|
114
|
-
{ name:
|
|
115
|
-
{ name:
|
|
116
|
-
{ name:
|
|
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:
|
|
121
|
-
base:
|
|
122
|
-
md:
|
|
123
|
-
lg:
|
|
124
|
-
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:
|
|
128
|
-
base:
|
|
129
|
-
slow:
|
|
130
|
-
timing:
|
|
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:
|
|
134
|
-
0:
|
|
135
|
-
10:
|
|
136
|
-
20:
|
|
137
|
-
30:
|
|
138
|
-
40:
|
|
139
|
-
50:
|
|
140
|
-
dropdown:
|
|
141
|
-
sticky:
|
|
142
|
-
fixed:
|
|
143
|
-
modal:
|
|
144
|
-
popover:
|
|
145
|
-
tooltip:
|
|
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
|
-
|
|
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(
|
|
153
|
-
console.log(
|
|
154
|
-
console.log(
|
|
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
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
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
|
-
//
|
|
166
|
-
|
|
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
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
494
|
+
if (!projectName || !projectName.trim()) {
|
|
495
|
+
console.log(chalk.red("\nProject name is required.\n"));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
174
498
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
499
|
+
// =========================================================================
|
|
500
|
+
// COLOURS
|
|
501
|
+
// =========================================================================
|
|
178
502
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
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
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
sourceDir = sourceDir.trim() || './src';
|
|
557
|
+
// =========================================================================
|
|
558
|
+
// TYPOGRAPHY
|
|
559
|
+
// =========================================================================
|
|
213
560
|
|
|
214
|
-
|
|
215
|
-
const config = createDefaultConfig(name, colours, fonts, baseUnit, sourceDir);
|
|
561
|
+
console.log(chalk.bold("\n" + chalk.magenta("→") + " Typography"));
|
|
216
562
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
606
|
+
// =========================================================================
|
|
607
|
+
// BUILD
|
|
608
|
+
// =========================================================================
|
|
230
609
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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:
|
|
628
|
+
stdio: "pipe",
|
|
629
|
+
shell: process.platform === "win32",
|
|
236
630
|
});
|
|
237
631
|
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|