emily-css 1.0.0
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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/emilyui.js +17 -0
- package/dist/emily.css +23068 -0
- package/dist/emily.demo.css +110 -0
- package/dist/emily.demo.min.css +1 -0
- package/dist/emily.min.css +1 -0
- package/dist/emily.purged.css +840 -0
- package/dist/emily.purged.min.css +1 -0
- package/fonts/inter/Inter-Variable.woff2 +0 -0
- package/fonts/lexend/Lexend-Variable.woff2 +0 -0
- package/package.json +42 -0
- package/src/generators.js +506 -0
- package/src/index.js +952 -0
- package/src/init.js +252 -0
- package/src/purge.js +194 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
// new version
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// COLOUR GENERATION
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Generate 10-shade colour scale using OKLCH (perceptually uniform colour space)
|
|
12
|
+
// OKLCH produces visually even steps across all hues — unlike HSL which creates
|
|
13
|
+
// muddy mid-tones on warm colours (yellows, greens, oranges).
|
|
14
|
+
//
|
|
15
|
+
// Conversion pipeline: Hex → sRGB → Linear RGB → OKLab → OKLCH → (modify L) → reverse
|
|
16
|
+
// No external dependencies. Maths from Björn Ottosson's OKLab specification.
|
|
17
|
+
//
|
|
18
|
+
// Input: #0077b6 → Output: { 10: '#...', 20: '#...', ..., 100: '#...' }
|
|
19
|
+
|
|
20
|
+
// sRGB component to linear light
|
|
21
|
+
function srgbToLinear(c) {
|
|
22
|
+
const val = c / 255;
|
|
23
|
+
return val <= 0.04045 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Linear light component to sRGB (clamped to 0–255)
|
|
27
|
+
function linearToSrgb(c) {
|
|
28
|
+
const clamped = Math.max(0, Math.min(1, c));
|
|
29
|
+
const out = clamped <= 0.0031308
|
|
30
|
+
? 12.92 * clamped
|
|
31
|
+
: 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
32
|
+
return Math.round(Math.max(0, Math.min(1, out)) * 255);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Hex string → OKLCH { l, c, h }
|
|
36
|
+
function hexToOklch(hex) {
|
|
37
|
+
const r = srgbToLinear(parseInt(hex.slice(1, 3), 16));
|
|
38
|
+
const g = srgbToLinear(parseInt(hex.slice(3, 5), 16));
|
|
39
|
+
const b = srgbToLinear(parseInt(hex.slice(5, 7), 16));
|
|
40
|
+
|
|
41
|
+
// Linear RGB → OKLab (M1 matrix then cube-root then M2 matrix)
|
|
42
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
43
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
44
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
45
|
+
|
|
46
|
+
const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
|
|
47
|
+
const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
|
|
48
|
+
const bv = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
|
|
49
|
+
|
|
50
|
+
// OKLab → OKLCH
|
|
51
|
+
const C = Math.sqrt(a * a + bv * bv);
|
|
52
|
+
const H = (Math.atan2(bv, a) * 180) / Math.PI;
|
|
53
|
+
|
|
54
|
+
return { l: L, c: C, h: H < 0 ? H + 360 : H };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// OKLCH { l, c, h } → hex string
|
|
58
|
+
function oklchToHex(l, c, h) {
|
|
59
|
+
// OKLCH → OKLab
|
|
60
|
+
const hRad = (h * Math.PI) / 180;
|
|
61
|
+
const a = c * Math.cos(hRad);
|
|
62
|
+
const bv = c * Math.sin(hRad);
|
|
63
|
+
|
|
64
|
+
// OKLab → Linear RGB (M2 inverse then cube then M1 inverse)
|
|
65
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * bv;
|
|
66
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * bv;
|
|
67
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * bv;
|
|
68
|
+
|
|
69
|
+
const lc = l_ * l_ * l_;
|
|
70
|
+
const mc = m_ * m_ * m_;
|
|
71
|
+
const sc = s_ * s_ * s_;
|
|
72
|
+
|
|
73
|
+
const r = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
|
|
74
|
+
const g = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
|
|
75
|
+
const b = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
76
|
+
|
|
77
|
+
const rOut = linearToSrgb(r).toString(16).padStart(2, '0');
|
|
78
|
+
const gOut = linearToSrgb(g).toString(16).padStart(2, '0');
|
|
79
|
+
const bOut = linearToSrgb(b).toString(16).padStart(2, '0');
|
|
80
|
+
|
|
81
|
+
return `#${rOut}${gOut}${bOut}`.toUpperCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function generateColourScale(baseHex) {
|
|
85
|
+
const { l: baseL, c: baseC, h: baseH } = hexToOklch(baseHex);
|
|
86
|
+
const scale = {};
|
|
87
|
+
|
|
88
|
+
// Shade scale: 10 = near-white, 80 = exact input colour, 100 = near-black
|
|
89
|
+
// Lightness targets in OKLCH (0–1 scale):
|
|
90
|
+
// shade 10 → L ≈ 0.97 (very light tint)
|
|
91
|
+
// shade 80 → L = baseL (exact input)
|
|
92
|
+
// shade 100 → L ≈ 0.15 (very dark tone)
|
|
93
|
+
//
|
|
94
|
+
// Chroma is preserved from the base colour throughout — hue is never shifted.
|
|
95
|
+
// At extreme lightness values chroma is gently reduced to stay in sRGB gamut.
|
|
96
|
+
|
|
97
|
+
const LIGHT_L = 0.97; // shade 10 lightness
|
|
98
|
+
const DARK_L = 0.15; // shade 100 lightness
|
|
99
|
+
|
|
100
|
+
const steps = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
101
|
+
|
|
102
|
+
steps.forEach(step => {
|
|
103
|
+
if (step === 80) {
|
|
104
|
+
scale[step] = baseHex.toUpperCase();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let newL;
|
|
109
|
+
if (step < 80) {
|
|
110
|
+
// 10–70: interpolate from LIGHT_L down to baseL
|
|
111
|
+
const t = (step / 80); // 0 → 1 as step goes 0 → 80
|
|
112
|
+
newL = LIGHT_L - t * (LIGHT_L - baseL);
|
|
113
|
+
} else {
|
|
114
|
+
// 90–100: interpolate from baseL down to DARK_L
|
|
115
|
+
const t = (step - 80) / 20; // 0 → 1 as step goes 80 → 100
|
|
116
|
+
newL = baseL - t * (baseL - DARK_L);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reduce chroma slightly at extremes to avoid out-of-gamut clipping
|
|
120
|
+
const chromaScale = 1 - Math.max(0, (newL - 0.90) / 0.07) * 0.5 // reduce near white
|
|
121
|
+
- Math.max(0, (0.25 - newL) / 0.10) * 0.5; // reduce near black
|
|
122
|
+
const newC = baseC * Math.max(0, chromaScale);
|
|
123
|
+
|
|
124
|
+
scale[step] = oklchToHex(newL, newC, baseH);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return scale;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function generateAllColours(colourConfig) {
|
|
131
|
+
const allColours = {};
|
|
132
|
+
|
|
133
|
+
Object.entries(colourConfig).forEach(([name, baseHex]) => {
|
|
134
|
+
allColours[name] = generateColourScale(baseHex);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return allColours;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// SPACING SCALE
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
function generateSpacing(baseUnit, scale) {
|
|
145
|
+
// scale is now a key-value object from config
|
|
146
|
+
// Just return it as-is since values are already defined (e.g., "1rem", "4px")
|
|
147
|
+
return scale;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// FONT PRESETS
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
// Bundled fonts live in fonts/{name}/ relative to the package root.
|
|
155
|
+
// The generated CSS lives in dist/, so relative paths use ../fonts/...
|
|
156
|
+
const FONT_PRESETS = {
|
|
157
|
+
'system': {
|
|
158
|
+
stack: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
159
|
+
bundled: false,
|
|
160
|
+
},
|
|
161
|
+
'inter': {
|
|
162
|
+
name: 'Inter',
|
|
163
|
+
stack: '"Inter", system-ui, sans-serif',
|
|
164
|
+
bundled: true,
|
|
165
|
+
file: '../fonts/inter/Inter-Variable.woff2',
|
|
166
|
+
weight: '100 900',
|
|
167
|
+
style: 'normal',
|
|
168
|
+
},
|
|
169
|
+
'lexend': {
|
|
170
|
+
name: 'Lexend',
|
|
171
|
+
stack: '"Lexend", system-ui, sans-serif',
|
|
172
|
+
bundled: true,
|
|
173
|
+
file: '../fonts/lexend/Lexend-Variable.woff2',
|
|
174
|
+
weight: '100 900',
|
|
175
|
+
style: 'normal',
|
|
176
|
+
},
|
|
177
|
+
'georgia': {
|
|
178
|
+
stack: 'Georgia, "Times New Roman", serif',
|
|
179
|
+
bundled: false,
|
|
180
|
+
},
|
|
181
|
+
'mono': {
|
|
182
|
+
stack: '"Menlo", "Monaco", "Courier New", monospace',
|
|
183
|
+
bundled: false,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function generateFontCSS(config) {
|
|
188
|
+
const fontFamily = (config.fontFamily || 'system').toLowerCase();
|
|
189
|
+
const preset = FONT_PRESETS[fontFamily] || FONT_PRESETS['system'];
|
|
190
|
+
|
|
191
|
+
let fontFace = '';
|
|
192
|
+
let bodyFont = '';
|
|
193
|
+
|
|
194
|
+
if (preset.bundled) {
|
|
195
|
+
fontFace += `@font-face {\n`;
|
|
196
|
+
fontFace += ` font-family: "${preset.name}";\n`;
|
|
197
|
+
fontFace += ` src: url("${preset.file}") format("woff2");\n`;
|
|
198
|
+
fontFace += ` font-weight: ${preset.weight};\n`;
|
|
199
|
+
fontFace += ` font-style: ${preset.style};\n`;
|
|
200
|
+
fontFace += ` font-display: swap;\n`;
|
|
201
|
+
fontFace += `}\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
bodyFont = ` body {\n font-family: ${preset.stack};\n font-synthesis: style;\n }\n`;
|
|
205
|
+
|
|
206
|
+
return { fontFace, bodyFont };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// CSS VARIABLE GENERATION
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
function generateCSSVariables(colours, spacing, config) {
|
|
214
|
+
let css = `:root {\n`;
|
|
215
|
+
|
|
216
|
+
// Colour variables
|
|
217
|
+
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
218
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
219
|
+
css += ` --color-${colourName}-${shade}: ${hex};\n`;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Spacing variables
|
|
224
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
225
|
+
css += ` --space-${key}: ${value};\n`;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Font size variables with line-height
|
|
229
|
+
config.typography.fontSizes.forEach(fontSize => {
|
|
230
|
+
const sizeVal = parseInt(fontSize.value);
|
|
231
|
+
css += ` --text-${fontSize.name}: ${fontSize.value};\n`;
|
|
232
|
+
css += ` --leading-${fontSize.name}: ${fontSize.lineHeight};\n`;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Font weight variables
|
|
236
|
+
Object.entries(config.typography.fontWeights).forEach(([name, weight]) => {
|
|
237
|
+
css += ` --font-weight-${name}: ${weight};\n`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Breakpoints
|
|
241
|
+
Object.entries(config.breakpoints).forEach(([name, value]) => {
|
|
242
|
+
css += ` --breakpoint-${name}: ${value};\n`;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Shadows
|
|
246
|
+
Object.entries(config.shadows).forEach(([name, shadow]) => {
|
|
247
|
+
css += ` --shadow-${name}: ${shadow};\n`;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Z-index
|
|
251
|
+
Object.entries(config.zIndex).forEach(([name, value]) => {
|
|
252
|
+
css += ` --z-${name}: ${value};\n`;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Transitions
|
|
256
|
+
css += ` --transition-duration: ${config.transitions.base};\n`;
|
|
257
|
+
css += ` --transition-timing: ${config.transitions.timing};\n`;
|
|
258
|
+
|
|
259
|
+
css += `}\n\n`;
|
|
260
|
+
return css;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const {
|
|
264
|
+
displayUtilities,
|
|
265
|
+
sizingUtilities,
|
|
266
|
+
positioningUtilities,
|
|
267
|
+
overflowUtilities,
|
|
268
|
+
opacityUtilities,
|
|
269
|
+
transitionUtilities,
|
|
270
|
+
transformUtilities,
|
|
271
|
+
shadowUtilities,
|
|
272
|
+
ringUtilities,
|
|
273
|
+
objectUtilities,
|
|
274
|
+
tableListUtilities,
|
|
275
|
+
svgUtilities,
|
|
276
|
+
formUtilities,
|
|
277
|
+
verticalAlignUtilities,
|
|
278
|
+
contentScrollUtilities,
|
|
279
|
+
blendUtilities,
|
|
280
|
+
cursorUtilities,
|
|
281
|
+
accessibilityUtilities,
|
|
282
|
+
containerUtilities
|
|
283
|
+
} = require('./generators');
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// SPACING UTILITIES
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
function escapeClassName(key) {
|
|
290
|
+
// Escape dots in class names for CSS (e.g., "0.5" becomes "0\.5")
|
|
291
|
+
return key.replace(/\./g, '\\.');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function generateSpacingUtilities(spacing) {
|
|
295
|
+
let css = `/* Spacing: Padding & Margin */\n`;
|
|
296
|
+
|
|
297
|
+
// Padding
|
|
298
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
299
|
+
const escaped = escapeClassName(key);
|
|
300
|
+
css += `.p-${escaped} { padding: ${value}; }\n`;
|
|
301
|
+
css += `.px-${escaped} { padding-left: ${value}; padding-right: ${value}; }\n`;
|
|
302
|
+
css += `.py-${escaped} { padding-top: ${value}; padding-bottom: ${value}; }\n`;
|
|
303
|
+
css += `.pt-${escaped} { padding-top: ${value}; }\n`;
|
|
304
|
+
css += `.pr-${escaped} { padding-right: ${value}; }\n`;
|
|
305
|
+
css += `.pb-${escaped} { padding-bottom: ${value}; }\n`;
|
|
306
|
+
css += `.pl-${escaped} { padding-left: ${value}; }\n`;
|
|
307
|
+
css += `.ps-${escaped} { padding-inline-start: ${value}; }\n`;
|
|
308
|
+
css += `.pe-${escaped} { padding-inline-end: ${value}; }\n`;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Margin
|
|
312
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
313
|
+
const escaped = escapeClassName(key);
|
|
314
|
+
css += `.m-${escaped} { margin: ${value}; }\n`;
|
|
315
|
+
css += `.mx-${escaped} { margin-left: ${value}; margin-right: ${value}; }\n`;
|
|
316
|
+
css += `.my-${escaped} { margin-top: ${value}; margin-bottom: ${value}; }\n`;
|
|
317
|
+
css += `.mt-${escaped} { margin-top: ${value}; }\n`;
|
|
318
|
+
css += `.mr-${escaped} { margin-right: ${value}; }\n`;
|
|
319
|
+
css += `.mb-${escaped} { margin-bottom: ${value}; }\n`;
|
|
320
|
+
css += `.ml-${escaped} { margin-left: ${value}; }\n`;
|
|
321
|
+
css += `.ms-${escaped} { margin-inline-start: ${value}; }\n`;
|
|
322
|
+
css += `.me-${escaped} { margin-inline-end: ${value}; }\n`;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Margin auto
|
|
326
|
+
css += `.mx-auto { margin-left: auto; margin-right: auto; }\n`;
|
|
327
|
+
css += `.my-auto { margin-top: auto; margin-bottom: auto; }\n`;
|
|
328
|
+
|
|
329
|
+
css += `\n`;
|
|
330
|
+
return css;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// FLEXBOX UTILITIES
|
|
335
|
+
// ============================================================================
|
|
336
|
+
|
|
337
|
+
function generateFlexboxUtilities(spacing) {
|
|
338
|
+
let css = `/* Flexbox */\n`;
|
|
339
|
+
|
|
340
|
+
// Note: .flex is already generated in displayUtilities(), removed duplicate here
|
|
341
|
+
css += `.inline-flex { display: inline-flex; }\n`;
|
|
342
|
+
|
|
343
|
+
// Direction
|
|
344
|
+
css += `.flex-row { flex-direction: row; }\n`;
|
|
345
|
+
css += `.flex-col { flex-direction: column; }\n`;
|
|
346
|
+
css += `.flex-row-reverse { flex-direction: row-reverse; }\n`;
|
|
347
|
+
css += `.flex-col-reverse { flex-direction: column-reverse; }\n`;
|
|
348
|
+
|
|
349
|
+
// Wrap
|
|
350
|
+
css += `.flex-wrap { flex-wrap: wrap; }\n`;
|
|
351
|
+
css += `.flex-nowrap { flex-wrap: nowrap; }\n`;
|
|
352
|
+
css += `.flex-wrap-reverse { flex-wrap: wrap-reverse; }\n`;
|
|
353
|
+
|
|
354
|
+
// Grow/shrink
|
|
355
|
+
css += `.flex-1 { flex: 1 1 0%; }\n`;
|
|
356
|
+
css += `.flex-auto { flex: 1 1 auto; }\n`;
|
|
357
|
+
css += `.flex-none { flex: none; }\n`;
|
|
358
|
+
css += `.grow { flex-grow: 1; }\n`;
|
|
359
|
+
css += `.grow-0 { flex-grow: 0; }\n`;
|
|
360
|
+
css += `.shrink { flex-shrink: 1; }\n`;
|
|
361
|
+
css += `.shrink-0 { flex-shrink: 0; }\n`;
|
|
362
|
+
|
|
363
|
+
// Justify (main axis)
|
|
364
|
+
css += `.justify-start { justify-content: flex-start; }\n`;
|
|
365
|
+
css += `.justify-end { justify-content: flex-end; }\n`;
|
|
366
|
+
css += `.justify-center { justify-content: center; }\n`;
|
|
367
|
+
css += `.justify-between { justify-content: space-between; }\n`;
|
|
368
|
+
css += `.justify-around { justify-content: space-around; }\n`;
|
|
369
|
+
css += `.justify-evenly { justify-content: space-evenly; }\n`;
|
|
370
|
+
|
|
371
|
+
// Items (cross axis)
|
|
372
|
+
css += `.items-start { align-items: flex-start; }\n`;
|
|
373
|
+
css += `.items-end { align-items: flex-end; }\n`;
|
|
374
|
+
css += `.items-center { align-items: center; }\n`;
|
|
375
|
+
css += `.items-baseline { align-items: baseline; }\n`;
|
|
376
|
+
css += `.items-stretch { align-items: stretch; }\n`;
|
|
377
|
+
|
|
378
|
+
// Self alignment
|
|
379
|
+
css += `.self-start { align-self: flex-start; }\n`;
|
|
380
|
+
css += `.self-end { align-self: flex-end; }\n`;
|
|
381
|
+
css += `.self-center { align-self: center; }\n`;
|
|
382
|
+
css += `.self-stretch { align-self: stretch; }\n`;
|
|
383
|
+
css += `.self-auto { align-self: auto; }\n`;
|
|
384
|
+
|
|
385
|
+
css += `\n`;
|
|
386
|
+
return css;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// GRID UTILITIES
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
function generateGridUtilities(spacing) {
|
|
394
|
+
let css = `/* Grid */\n`;
|
|
395
|
+
|
|
396
|
+
// Note: .grid is already generated in displayUtilities(), removed duplicate here
|
|
397
|
+
css += `.inline-grid { display: inline-grid; }\n`;
|
|
398
|
+
|
|
399
|
+
// Grid columns
|
|
400
|
+
for (let i = 1; i <= 12; i++) {
|
|
401
|
+
css += `.grid-cols-${i} { grid-template-columns: repeat(${i}, minmax(0, 1fr)); }\n`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Column span
|
|
405
|
+
for (let i = 1; i <= 12; i++) {
|
|
406
|
+
css += `.col-span-${i} { grid-column: span ${i} / span ${i}; }\n`;
|
|
407
|
+
}
|
|
408
|
+
css += `.col-span-full { grid-column: 1 / -1; }\n`;
|
|
409
|
+
|
|
410
|
+
// Column start/end
|
|
411
|
+
for (let i = 1; i <= 13; i++) {
|
|
412
|
+
css += `.col-start-${i} { grid-column-start: ${i}; }\n`;
|
|
413
|
+
css += `.col-end-${i} { grid-column-end: ${i}; }\n`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Row span
|
|
417
|
+
for (let i = 1; i <= 6; i++) {
|
|
418
|
+
css += `.row-span-${i} { grid-row: span ${i} / span ${i}; }\n`;
|
|
419
|
+
}
|
|
420
|
+
css += `.row-span-full { grid-row: 1 / -1; }\n`;
|
|
421
|
+
|
|
422
|
+
// Row start/end
|
|
423
|
+
for (let i = 1; i <= 6; i++) {
|
|
424
|
+
css += `.row-start-${i} { grid-row-start: ${i}; }\n`;
|
|
425
|
+
css += `.row-end-${i} { grid-row-end: ${i}; }\n`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Auto flow
|
|
429
|
+
css += `.auto-cols-auto { grid-auto-columns: auto; }\n`;
|
|
430
|
+
css += `.auto-cols-fr { grid-auto-columns: minmax(0, 1fr); }\n`;
|
|
431
|
+
css += `.auto-rows-auto { grid-auto-rows: auto; }\n`;
|
|
432
|
+
css += `.auto-rows-fr { grid-auto-rows: minmax(0, 1fr); }\n`;
|
|
433
|
+
|
|
434
|
+
// Gap
|
|
435
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
436
|
+
const escaped = escapeClassName(key);
|
|
437
|
+
css += `.gap-${escaped} { gap: ${value}; }\n`;
|
|
438
|
+
css += `.gap-x-${escaped} { column-gap: ${value}; }\n`;
|
|
439
|
+
css += `.gap-y-${escaped} { row-gap: ${value}; }\n`;
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
css += `\n`;
|
|
443
|
+
return css;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// TYPOGRAPHY UTILITIES
|
|
448
|
+
// ============================================================================
|
|
449
|
+
|
|
450
|
+
function generateTypographyUtilities(config) {
|
|
451
|
+
let css = `/* Typography */\n`;
|
|
452
|
+
|
|
453
|
+
// Font sizes
|
|
454
|
+
config.typography.fontSizes.forEach(fontSize => {
|
|
455
|
+
css += `.text-${fontSize.name} { font-size: var(--text-${fontSize.name}); line-height: ${fontSize.lineHeight}; }\n`;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Font weights
|
|
459
|
+
Object.entries(config.typography.fontWeights).forEach(([name, weight]) => {
|
|
460
|
+
css += `.font-${name} { font-weight: ${weight}; }\n`;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Text alignment
|
|
464
|
+
css += `.text-left { text-align: left; }\n`;
|
|
465
|
+
css += `.text-center { text-align: center; }\n`;
|
|
466
|
+
css += `.text-right { text-align: right; }\n`;
|
|
467
|
+
css += `.text-justify { text-align: justify; }\n`;
|
|
468
|
+
|
|
469
|
+
// Text wrapping
|
|
470
|
+
css += `.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n`;
|
|
471
|
+
css += `.whitespace-nowrap { white-space: nowrap; }\n`;
|
|
472
|
+
css += `.whitespace-normal { white-space: normal; }\n`;
|
|
473
|
+
css += `.break-words { word-break: break-word; }\n`;
|
|
474
|
+
css += `.break-all { word-break: break-all; }\n`;
|
|
475
|
+
|
|
476
|
+
// Line height
|
|
477
|
+
css += `.leading-tight { line-height: 1.2; }\n`;
|
|
478
|
+
css += `.leading-normal { line-height: 1.5; }\n`;
|
|
479
|
+
css += `.leading-relaxed { line-height: 1.75; }\n`;
|
|
480
|
+
|
|
481
|
+
// Letter spacing
|
|
482
|
+
css += `.tracking-tighter { letter-spacing: -0.05em; }\n`;
|
|
483
|
+
css += `.tracking-tight { letter-spacing: -0.02em; }\n`;
|
|
484
|
+
css += `.tracking-normal { letter-spacing: 0em; }\n`;
|
|
485
|
+
css += `.tracking-wide { letter-spacing: 0.02em; }\n`;
|
|
486
|
+
css += `.tracking-wider { letter-spacing: 0.05em; }\n`;
|
|
487
|
+
css += `.tracking-widest { letter-spacing: 0.1em; }\n`;
|
|
488
|
+
|
|
489
|
+
// Text decoration
|
|
490
|
+
css += `.underline { text-decoration: underline; }\n`;
|
|
491
|
+
css += `.no-underline { text-decoration: none; }\n`;
|
|
492
|
+
css += `.line-through { text-decoration: line-through; }\n`;
|
|
493
|
+
|
|
494
|
+
// Text transform
|
|
495
|
+
css += `.uppercase { text-transform: uppercase; }\n`;
|
|
496
|
+
css += `.lowercase { text-transform: lowercase; }\n`;
|
|
497
|
+
css += `.capitalize { text-transform: capitalize; }\n`;
|
|
498
|
+
|
|
499
|
+
// Font family utilities
|
|
500
|
+
css += `.font-sans { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n`;
|
|
501
|
+
css += `.font-serif { font-family: Georgia, "Times New Roman", serif; }\n`;
|
|
502
|
+
css += `.font-mono { font-family: "Menlo", "Monaco", "Courier New", monospace; }\n`;
|
|
503
|
+
css += `.font-inter { font-family: "Inter", system-ui, sans-serif; }\n`;
|
|
504
|
+
css += `.font-lexend { font-family: "Lexend", system-ui, sans-serif; }\n`;
|
|
505
|
+
|
|
506
|
+
css += `\n`;
|
|
507
|
+
return css;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ============================================================================
|
|
511
|
+
// BORDER UTILITIES
|
|
512
|
+
// ============================================================================
|
|
513
|
+
|
|
514
|
+
function generateBorderUtilities(config) {
|
|
515
|
+
let css = `/* Borders & Radius */\n`;
|
|
516
|
+
|
|
517
|
+
// Border widths from config
|
|
518
|
+
const borderWidths = config.spacing.borderWidths || [0, 2, 4, 8];
|
|
519
|
+
|
|
520
|
+
// Border width
|
|
521
|
+
css += `.border { border-width: 1px; }\n`;
|
|
522
|
+
borderWidths.forEach(width => {
|
|
523
|
+
css += `.border-${width} { border-width: ${width}px; }\n`;
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Border sides (default 1px + widths)
|
|
527
|
+
|
|
528
|
+
['t', 'r', 'b', 'l'].forEach(side => {
|
|
529
|
+
css += `.border-${side} { border-${side === 't' ? 'top' : side === 'r' ? 'right' : side === 'b' ? 'bottom' : 'left'}-width: 1px; }\n`;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
borderWidths.forEach(width => {
|
|
533
|
+
['t', 'r', 'b', 'l'].forEach(side => {
|
|
534
|
+
const sideMap = { t: 'top', r: 'right', b: 'bottom', l: 'left' };
|
|
535
|
+
css += `.border-${side}-${width} { border-${sideMap[side]}-width: ${width}px; }\n`;
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Border style
|
|
540
|
+
css += `.border-solid { border-style: solid; }\n`;
|
|
541
|
+
css += `.border-dashed { border-style: dashed; }\n`;
|
|
542
|
+
css += `.border-dotted { border-style: dotted; }\n`;
|
|
543
|
+
css += `.border-double { border-style: double; }\n`;
|
|
544
|
+
css += `.border-none { border-style: none; }\n`;
|
|
545
|
+
|
|
546
|
+
// Border radius (base)
|
|
547
|
+
const baseRadius = config.spacing.borderRadius['base'] || '8px';
|
|
548
|
+
css += `.rounded { border-radius: ${baseRadius}; }\n`;
|
|
549
|
+
|
|
550
|
+
// Border radius (named)
|
|
551
|
+
Object.entries(config.spacing.borderRadius).forEach(([name, value]) => {
|
|
552
|
+
css += `.rounded-${name} { border-radius: ${value}; }\n`;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Border radius per side
|
|
556
|
+
css += `.rounded-t { border-top-left-radius: ${baseRadius}; border-top-right-radius: ${baseRadius}; }\n`;
|
|
557
|
+
css += `.rounded-b { border-bottom-left-radius: ${baseRadius}; border-bottom-right-radius: ${baseRadius}; }\n`;
|
|
558
|
+
css += `.rounded-l { border-top-left-radius: ${baseRadius}; border-bottom-left-radius: ${baseRadius}; }\n`;
|
|
559
|
+
css += `.rounded-r { border-top-right-radius: ${baseRadius}; border-bottom-right-radius: ${baseRadius}; }\n`;
|
|
560
|
+
css += `.rounded-tl { border-top-left-radius: ${baseRadius}; }\n`;
|
|
561
|
+
css += `.rounded-tr { border-top-right-radius: ${baseRadius}; }\n`;
|
|
562
|
+
css += `.rounded-bl { border-bottom-left-radius: ${baseRadius}; }\n`;
|
|
563
|
+
css += `.rounded-br { border-bottom-right-radius: ${baseRadius}; }\n`;
|
|
564
|
+
|
|
565
|
+
css += `\n`;
|
|
566
|
+
return css;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// BASE ELEMENT STYLES
|
|
571
|
+
// ============================================================================
|
|
572
|
+
|
|
573
|
+
function generateBaseStyles(config) {
|
|
574
|
+
const baseStyles = config.baseStyles;
|
|
575
|
+
if (!baseStyles || Object.keys(baseStyles).length === 0) return '';
|
|
576
|
+
|
|
577
|
+
// Build a lookup map from font size name → CSS variable
|
|
578
|
+
const fontSizeMap = {};
|
|
579
|
+
(config.typography?.fontSizes || []).forEach(({ name }) => {
|
|
580
|
+
fontSizeMap[name] = `var(--emily-text-${name})`;
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Line height hints per size — keeps headings tighter than body text
|
|
584
|
+
const lineHeightMap = {
|
|
585
|
+
xs: '1.5', sm: '1.5', base: '1.6', lg: '1.6',
|
|
586
|
+
xl: '1.5', '2xl': '1.4', '3xl': '1.35', '4xl': '1.3'
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
let css = '\n /* Base element styles (from baseStyles in emily.config.json) */\n';
|
|
590
|
+
Object.entries(baseStyles).forEach(([element, sizeKey]) => {
|
|
591
|
+
const varRef = fontSizeMap[sizeKey];
|
|
592
|
+
if (!varRef) return;
|
|
593
|
+
const lh = lineHeightMap[sizeKey] || '1.5';
|
|
594
|
+
css += ` ${element} { font-size: ${varRef}; line-height: ${lh}; }\n`;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return css;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ============================================================================
|
|
601
|
+
// COLOUR UTILITIES
|
|
602
|
+
// ============================================================================
|
|
603
|
+
|
|
604
|
+
function generateColourUtilities(colours) {
|
|
605
|
+
let css = `/* Colours: Background, Text, Borders, Accents */\n`;
|
|
606
|
+
|
|
607
|
+
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
608
|
+
// Background colours
|
|
609
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
610
|
+
css += `.bg-${colourName}-${shade} { background-color: ${hex}; }\n`;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Text colours
|
|
614
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
615
|
+
css += `.text-${colourName}-${shade} { color: ${hex}; }\n`;
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Border colours
|
|
619
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
620
|
+
css += `.border-${colourName}-${shade} { border-color: ${hex}; }\n`;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Accent colours (for form elements like checkboxes, radio buttons)
|
|
624
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
625
|
+
css += `.accent-${colourName}-${shade} { accent-color: ${hex}; }\n`;
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
css += `.bg-white { background-color: #ffffff; }\n`;
|
|
630
|
+
css += `.bg-transparent { background-color: transparent; }\n`;
|
|
631
|
+
css += `.text-white { color: #ffffff; }\n`;
|
|
632
|
+
|
|
633
|
+
css += `\n`;
|
|
634
|
+
return css;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ============================================================================
|
|
638
|
+
// DARK MODE VARIANTS
|
|
639
|
+
// ============================================================================
|
|
640
|
+
// Generates dark: prefixed versions of colour and appearance utilities only.
|
|
641
|
+
// Layout, spacing, and typography utilities don't change in dark mode —
|
|
642
|
+
// targeting only the utilities where dark mode actually makes a difference
|
|
643
|
+
// keeps the output lean and the purge step effective.
|
|
644
|
+
//
|
|
645
|
+
// Usage in HTML: class="bg-neutral-10 dark:bg-neutral-90 text-neutral-90 dark:text-neutral-10"
|
|
646
|
+
// Output: @media (prefers-color-scheme: dark) { .dark\:bg-neutral-90 { background-color: ...; } }
|
|
647
|
+
|
|
648
|
+
function addDarkModeVariants(css) {
|
|
649
|
+
// Match on CSS property, not class name prefix — avoids catching
|
|
650
|
+
// structural utilities like text-xs (font-size) or text-left (text-align)
|
|
651
|
+
// when we only want colour-related declarations.
|
|
652
|
+
const colourProperties = [
|
|
653
|
+
'background-color',
|
|
654
|
+
'color',
|
|
655
|
+
'border-color',
|
|
656
|
+
'accent-color',
|
|
657
|
+
'box-shadow',
|
|
658
|
+
'opacity',
|
|
659
|
+
'fill',
|
|
660
|
+
'stroke',
|
|
661
|
+
'--tw-ring-color',
|
|
662
|
+
'outline-color',
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
let darkRules = '';
|
|
666
|
+
const lines = css.split('\n');
|
|
667
|
+
|
|
668
|
+
lines.forEach(line => {
|
|
669
|
+
if (line.startsWith('.') && line.includes('{') && line.includes('}')) {
|
|
670
|
+
const className = line.split('{')[0].trim();
|
|
671
|
+
|
|
672
|
+
// Only base utilities — skip anything already a variant (contains ':')
|
|
673
|
+
if (className.includes(':')) return;
|
|
674
|
+
|
|
675
|
+
// Only colour/appearance properties
|
|
676
|
+
const isColourUtility = colourProperties.some(prop => line.includes(prop + ':'));
|
|
677
|
+
if (!isColourUtility) return;
|
|
678
|
+
|
|
679
|
+
const classWithoutDot = className.substring(1);
|
|
680
|
+
const darkRule = line.replace(className, `.dark\\:${classWithoutDot}`);
|
|
681
|
+
darkRules += ' ' + darkRule + '\n';
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
if (!darkRules) return css;
|
|
686
|
+
|
|
687
|
+
return css
|
|
688
|
+
+ `\n/* Dark mode variants — explicit override */\n[data-theme="dark"] {\n${darkRules}}\n`
|
|
689
|
+
+ `\n/* Dark mode variants — system preference (no override set) */\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n${darkRules} }\n}\n`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ============================================================================
|
|
693
|
+
// RESPONSIVE VARIANTS
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
function addResponsiveVariants(css, config) {
|
|
697
|
+
let variantCss = css;
|
|
698
|
+
|
|
699
|
+
Object.entries(config.breakpoints).forEach(([breakpointName, breakpointValue]) => {
|
|
700
|
+
const mediaQuery = `@media (min-width: ${breakpointValue}) {\n`;
|
|
701
|
+
let breakpointRules = '';
|
|
702
|
+
|
|
703
|
+
// Extract all utility rules and add responsive prefix
|
|
704
|
+
const lines = css.split('\n');
|
|
705
|
+
lines.forEach(line => {
|
|
706
|
+
if (line.startsWith('.') && line.includes('{')) {
|
|
707
|
+
const className = line.split('{')[0].trim();
|
|
708
|
+
const rule = line;
|
|
709
|
+
// Skip variables and already responsive selectors
|
|
710
|
+
if (!className.startsWith(':root') && !className.includes(':')) {
|
|
711
|
+
const responsiveRule = rule.replace(className, `.${breakpointName}\\:${className.substring(1)}`);
|
|
712
|
+
breakpointRules += ' ' + responsiveRule + '\n';
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
if (breakpointRules) {
|
|
718
|
+
variantCss += mediaQuery + breakpointRules + '}\n\n';
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
return variantCss;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ============================================================================
|
|
726
|
+
// STATE VARIANTS
|
|
727
|
+
// ============================================================================
|
|
728
|
+
// Add pseudo-class variants for hover, focus-visible, active, disabled
|
|
729
|
+
|
|
730
|
+
function addStateVariants(css) {
|
|
731
|
+
const states = [
|
|
732
|
+
{ name: 'hover', selector: ':hover' },
|
|
733
|
+
{ name: 'focus', selector: ':focus' },
|
|
734
|
+
{ name: 'focus-visible', selector: ':focus-visible' },
|
|
735
|
+
{ name: 'active', selector: ':active' },
|
|
736
|
+
{ name: 'disabled', selector: ':disabled' }
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
let variantCss = css;
|
|
740
|
+
|
|
741
|
+
states.forEach(state => {
|
|
742
|
+
let stateRules = '';
|
|
743
|
+
|
|
744
|
+
// Extract all utility rules and add state prefix
|
|
745
|
+
const lines = css.split('\n');
|
|
746
|
+
lines.forEach(line => {
|
|
747
|
+
if (line.startsWith('.') && line.includes('{')) {
|
|
748
|
+
const className = line.split('{')[0].trim();
|
|
749
|
+
// Skip variables, media queries, pseudo-elements, and state variants
|
|
750
|
+
if (!className.startsWith(':root') && !className.includes('@') && !className.includes('::') && !className.includes(':')) {
|
|
751
|
+
// Generate state variant: .hover\:block:hover { display: block; }
|
|
752
|
+
// Remove leading dot from className, add state prefix with escaped colon
|
|
753
|
+
const classWithoutDot = className.substring(1);
|
|
754
|
+
const stateSelector = `.${state.name}\\:${classWithoutDot}${state.selector}`;
|
|
755
|
+
const statefulRule = line.replace(className, stateSelector);
|
|
756
|
+
stateRules += statefulRule + '\n';
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
if (stateRules) {
|
|
762
|
+
variantCss += '\n/* State variant: ' + state.name + ' */\n' + stateRules;
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
return variantCss;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ============================================================================
|
|
770
|
+
// BUILD FUNCTION
|
|
771
|
+
// ============================================================================
|
|
772
|
+
|
|
773
|
+
// ============================================================================
|
|
774
|
+
// BUILD FUNCTION
|
|
775
|
+
// ============================================================================
|
|
776
|
+
|
|
777
|
+
function build(options = {}) {
|
|
778
|
+
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
779
|
+
if (!fs.existsSync(configPath)) {
|
|
780
|
+
console.error(`\n emily-ui: No config found.\n Expected: ${configPath}\n Run "emily-ui init" to create one.\n`);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
784
|
+
|
|
785
|
+
if (options.purge) {
|
|
786
|
+
const { purgeCSS } = require('./purge.js');
|
|
787
|
+
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
788
|
+
if (!fs.existsSync(cssPath)) {
|
|
789
|
+
console.error(' emily-ui: Run emily-ui build first before purging.');
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
console.log(`Purging unused utilities from ${options.purge}...`);
|
|
793
|
+
const css = fs.readFileSync(cssPath, 'utf8');
|
|
794
|
+
const purged = purgeCSS(css, options.purge);
|
|
795
|
+
const minified = purged.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s?\{/g, '{').replace(/\s?\}/g, '}').replace(/;\s/g, ';').trim();
|
|
796
|
+
fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.css'), purged);
|
|
797
|
+
fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.min.css'), minified);
|
|
798
|
+
console.log('✓ Purged CSS: dist/emily.purged.css');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
console.log('Building EmilyUI...');
|
|
803
|
+
|
|
804
|
+
// Generate colours
|
|
805
|
+
const colours = generateAllColours(config.colours);
|
|
806
|
+
console.log(`✓ Generated ${Object.keys(colours).length} colour scales`);
|
|
807
|
+
|
|
808
|
+
// Generate spacing
|
|
809
|
+
const spacing = generateSpacing(config.baseUnit, config.spacing.scale);
|
|
810
|
+
console.log(`✓ Generated ${Object.keys(spacing).length} spacing values`);
|
|
811
|
+
|
|
812
|
+
// 1. Generate Variables (Theme Layer)
|
|
813
|
+
const variablesCss = generateCSSVariables(colours, spacing, config);
|
|
814
|
+
|
|
815
|
+
// 2. Generate Utilities (Utilities Layer)
|
|
816
|
+
let utilityCss = '';
|
|
817
|
+
utilityCss += displayUtilities();
|
|
818
|
+
utilityCss += generateSpacingUtilities(spacing);
|
|
819
|
+
utilityCss += generateFlexboxUtilities(spacing);
|
|
820
|
+
utilityCss += generateGridUtilities(spacing);
|
|
821
|
+
utilityCss += sizingUtilities(spacing);
|
|
822
|
+
utilityCss += generateTypographyUtilities(config);
|
|
823
|
+
utilityCss += generateBorderUtilities(config);
|
|
824
|
+
utilityCss += generateColourUtilities(colours);
|
|
825
|
+
utilityCss += positioningUtilities(spacing);
|
|
826
|
+
utilityCss += overflowUtilities();
|
|
827
|
+
utilityCss += transformUtilities(spacing);
|
|
828
|
+
utilityCss += shadowUtilities();
|
|
829
|
+
utilityCss += ringUtilities(colours);
|
|
830
|
+
utilityCss += objectUtilities();
|
|
831
|
+
utilityCss += tableListUtilities();
|
|
832
|
+
utilityCss += svgUtilities(colours);
|
|
833
|
+
utilityCss += formUtilities();
|
|
834
|
+
utilityCss += verticalAlignUtilities();
|
|
835
|
+
utilityCss += contentScrollUtilities();
|
|
836
|
+
utilityCss += opacityUtilities();
|
|
837
|
+
utilityCss += transitionUtilities();
|
|
838
|
+
utilityCss += blendUtilities();
|
|
839
|
+
utilityCss += cursorUtilities();
|
|
840
|
+
utilityCss += accessibilityUtilities();
|
|
841
|
+
utilityCss += containerUtilities();
|
|
842
|
+
|
|
843
|
+
// Add state, dark mode, and responsive variants to utilities
|
|
844
|
+
utilityCss = addStateVariants(utilityCss);
|
|
845
|
+
utilityCss = addDarkModeVariants(utilityCss);
|
|
846
|
+
utilityCss = addResponsiveVariants(utilityCss, config);
|
|
847
|
+
|
|
848
|
+
// 3. Assemble Final CSS with Layers
|
|
849
|
+
// Layer order matters: later layers win over earlier ones.
|
|
850
|
+
// theme → CSS custom properties / design tokens
|
|
851
|
+
// base → box-sizing reset, font-face, body defaults
|
|
852
|
+
// components → reserved for future component styles
|
|
853
|
+
// utilities → generated utility classes (highest priority)
|
|
854
|
+
const { fontFace, bodyFont } = generateFontCSS(config);
|
|
855
|
+
console.log(`✓ Font: ${config.fontFamily || 'system'}`);
|
|
856
|
+
|
|
857
|
+
const baseCss = `
|
|
858
|
+
/* Box sizing */
|
|
859
|
+
*, *::before, *::after {
|
|
860
|
+
box-sizing: border-box;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* Remove default margin/padding on common elements */
|
|
864
|
+
body, h1, h2, h3, h4, h5, h6, p,
|
|
865
|
+
ul, ol, dl, dd, figure, blockquote,
|
|
866
|
+
fieldset, textarea, pre {
|
|
867
|
+
margin: 0;
|
|
868
|
+
padding: 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* Lists: remove bullets/numbers when unstyled */
|
|
872
|
+
ul, ol {
|
|
873
|
+
list-style: none;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* Inherit fonts for form elements */
|
|
877
|
+
input, button, textarea, select {
|
|
878
|
+
font: inherit;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* Sensible media defaults */
|
|
882
|
+
img, picture, video, canvas, svg {
|
|
883
|
+
display: block;
|
|
884
|
+
max-width: 100%;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* Remove default button styles */
|
|
888
|
+
button {
|
|
889
|
+
background: none;
|
|
890
|
+
border: none;
|
|
891
|
+
cursor: pointer;
|
|
892
|
+
padding: 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Avoid overflow on long words */
|
|
896
|
+
p, h1, h2, h3, h4, h5, h6 {
|
|
897
|
+
overflow-wrap: break-word;
|
|
898
|
+
}
|
|
899
|
+
${bodyFont}`;
|
|
900
|
+
|
|
901
|
+
// @font-face must sit outside @layer for broadest browser compatibility
|
|
902
|
+
let css = fontFace ? `${fontFace}\n` : '';
|
|
903
|
+
css += `@layer theme, base, components, utilities;\n\n`;
|
|
904
|
+
css += `@layer theme {\n${variablesCss}}\n\n`;
|
|
905
|
+
const baseStylesCss = generateBaseStyles(config);
|
|
906
|
+
css += `@layer base {${baseCss}${baseStylesCss}}\n\n`;
|
|
907
|
+
css += `@layer components {\n /* Reserved for component styles in a future release. */\n}\n\n`;
|
|
908
|
+
css += `@layer utilities {\n${utilityCss}}\n`;
|
|
909
|
+
|
|
910
|
+
// Write output
|
|
911
|
+
const outputPath = path.join(process.cwd(), 'dist/emily.css');
|
|
912
|
+
const outputDir = path.dirname(outputPath);
|
|
913
|
+
|
|
914
|
+
if (!fs.existsSync(outputDir)) {
|
|
915
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
fs.writeFileSync(outputPath, css);
|
|
919
|
+
console.log(`✓ Generated CSS: ${outputPath}`);
|
|
920
|
+
console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
|
|
921
|
+
|
|
922
|
+
// Generate minified version
|
|
923
|
+
const minified = css.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s?\{/g, '{').replace(/\s?\}/g, '}').replace(/;\s/g, ';').trim();
|
|
924
|
+
const minPath = path.join(process.cwd(), 'dist/emily.min.css');
|
|
925
|
+
fs.writeFileSync(minPath, minified);
|
|
926
|
+
console.log(' -> Minified: ' + minPath);
|
|
927
|
+
console.log(' -> File size: ' + (minified.length / 1024).toFixed(2) + ' KB (minified)');
|
|
928
|
+
|
|
929
|
+
console.log('Build complete');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (require.main === module) {
|
|
933
|
+
const args = process.argv.slice(2);
|
|
934
|
+
const purgeIndex = args.indexOf('--purge');
|
|
935
|
+
const purgeDir = purgeIndex !== -1 ? args[purgeIndex + 1] : null;
|
|
936
|
+
build(purgeDir ? { purge: purgeDir } : {});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
module.exports = {
|
|
940
|
+
build,
|
|
941
|
+
hexToOklch,
|
|
942
|
+
oklchToHex,
|
|
943
|
+
generateColourScale,
|
|
944
|
+
generateAllColours,
|
|
945
|
+
generateSpacing,
|
|
946
|
+
generateBorderUtilities,
|
|
947
|
+
generateColourUtilities,
|
|
948
|
+
generateTypographyUtilities,
|
|
949
|
+
generateSpacingUtilities,
|
|
950
|
+
addStateVariants,
|
|
951
|
+
addResponsiveVariants,
|
|
952
|
+
};
|