emily-css 1.2.8 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/index.js +1890 -1886
- package/src/init.js +1011 -997
package/src/index.js
CHANGED
|
@@ -1,1886 +1,1890 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { generateManifest } = require('./manifest');
|
|
6
|
-
const { generateIntellisense } = require('./intellisense');
|
|
7
|
-
const {
|
|
8
|
-
getConfigPath,
|
|
9
|
-
getConfig,
|
|
10
|
-
getFullCssPath,
|
|
11
|
-
getProductionCssPath,
|
|
12
|
-
getManifestSettings,
|
|
13
|
-
getManifestOutputPath,
|
|
14
|
-
getIntellisenseSettings,
|
|
15
|
-
getIntellisenseOutputPath,
|
|
16
|
-
ensureDirectoryForFile,
|
|
17
|
-
getSourceDir,
|
|
18
|
-
} = require('./config');
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// COLOUR GENERATION
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Generate 10-shade colour scale using OKLCH (perceptually uniform colour space)
|
|
25
|
-
// OKLCH produces visually even steps across all hues — unlike HSL which creates
|
|
26
|
-
// muddy mid-tones on warm colours (yellows, greens, oranges).
|
|
27
|
-
//
|
|
28
|
-
// Conversion pipeline: Hex → sRGB → Linear RGB → OKLab → OKLCH → (modify L) → reverse
|
|
29
|
-
// No external dependencies. Maths from Björn Ottosson's OKLab specification.
|
|
30
|
-
//
|
|
31
|
-
// Input: #0077b6 → Output: { 10: '#...', 20: '#...', ..., 100: '#...' }
|
|
32
|
-
|
|
33
|
-
// sRGB component to linear light
|
|
34
|
-
function srgbToLinear(c) {
|
|
35
|
-
const val = c / 255;
|
|
36
|
-
return val <= 0.04045 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Linear light component to sRGB (clamped to 0–255)
|
|
40
|
-
function linearToSrgb(c) {
|
|
41
|
-
const clamped = Math.max(0, Math.min(1, c));
|
|
42
|
-
const out = clamped <= 0.0031308
|
|
43
|
-
? 12.92 * clamped
|
|
44
|
-
: 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
45
|
-
return Math.round(Math.max(0, Math.min(1, out)) * 255);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Hex string → OKLCH { l, c, h }
|
|
49
|
-
function hexToOklch(hex) {
|
|
50
|
-
const r = srgbToLinear(parseInt(hex.slice(1, 3), 16));
|
|
51
|
-
const g = srgbToLinear(parseInt(hex.slice(3, 5), 16));
|
|
52
|
-
const b = srgbToLinear(parseInt(hex.slice(5, 7), 16));
|
|
53
|
-
|
|
54
|
-
// Linear RGB → OKLab (M1 matrix then cube-root then M2 matrix)
|
|
55
|
-
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
56
|
-
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
57
|
-
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
58
|
-
|
|
59
|
-
const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
|
|
60
|
-
const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
|
|
61
|
-
const bv = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
|
|
62
|
-
|
|
63
|
-
// OKLab → OKLCH
|
|
64
|
-
const C = Math.sqrt(a * a + bv * bv);
|
|
65
|
-
const H = (Math.atan2(bv, a) * 180) / Math.PI;
|
|
66
|
-
|
|
67
|
-
return { l: L, c: C, h: H < 0 ? H + 360 : H };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// OKLCH { l, c, h } → hex string
|
|
71
|
-
function oklchToHex(l, c, h) {
|
|
72
|
-
// OKLCH → OKLab
|
|
73
|
-
const hRad = (h * Math.PI) / 180;
|
|
74
|
-
const a = c * Math.cos(hRad);
|
|
75
|
-
const bv = c * Math.sin(hRad);
|
|
76
|
-
|
|
77
|
-
// OKLab → Linear RGB (M2 inverse then cube then M1 inverse)
|
|
78
|
-
const l_ = l + 0.3963377774 * a + 0.2158037573 * bv;
|
|
79
|
-
const m_ = l - 0.1055613458 * a - 0.0638541728 * bv;
|
|
80
|
-
const s_ = l - 0.0894841775 * a - 1.2914855480 * bv;
|
|
81
|
-
|
|
82
|
-
const lc = l_ * l_ * l_;
|
|
83
|
-
const mc = m_ * m_ * m_;
|
|
84
|
-
const sc = s_ * s_ * s_;
|
|
85
|
-
|
|
86
|
-
const r = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
|
|
87
|
-
const g = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
|
|
88
|
-
const b = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
89
|
-
|
|
90
|
-
const rOut = linearToSrgb(r).toString(16).padStart(2, '0');
|
|
91
|
-
const gOut = linearToSrgb(g).toString(16).padStart(2, '0');
|
|
92
|
-
const bOut = linearToSrgb(b).toString(16).padStart(2, '0');
|
|
93
|
-
|
|
94
|
-
return `#${rOut}${gOut}${bOut}`.toUpperCase();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function generateColourScale(baseHex) {
|
|
98
|
-
const { l: baseL, c: baseC, h: baseH } = hexToOklch(baseHex);
|
|
99
|
-
const scale = {};
|
|
100
|
-
|
|
101
|
-
// Shade scale: 10 = near-white, 80 = exact input colour, 100 = near-black
|
|
102
|
-
// Lightness targets in OKLCH (0–1 scale):
|
|
103
|
-
// shade 10 → L ≈ 0.97 (very light tint)
|
|
104
|
-
// shade 80 → L = baseL (exact input)
|
|
105
|
-
// shade 100 → L ≈ 0.15 (very dark tone)
|
|
106
|
-
//
|
|
107
|
-
// Chroma is preserved from the base colour throughout — hue is never shifted.
|
|
108
|
-
// At extreme lightness values chroma is gently reduced to stay in sRGB gamut.
|
|
109
|
-
|
|
110
|
-
const LIGHT_L = 0.97; // shade 10 lightness
|
|
111
|
-
const DARK_L = 0.15; // shade 100 lightness
|
|
112
|
-
|
|
113
|
-
const steps = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
114
|
-
|
|
115
|
-
steps.forEach(step => {
|
|
116
|
-
if (step === 80) {
|
|
117
|
-
scale[step] = baseHex.toUpperCase();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
let newL;
|
|
122
|
-
if (step < 80) {
|
|
123
|
-
// 10–70: interpolate from LIGHT_L down to baseL
|
|
124
|
-
const t = (step / 80); // 0 → 1 as step goes 0 → 80
|
|
125
|
-
newL = LIGHT_L - t * (LIGHT_L - baseL);
|
|
126
|
-
} else {
|
|
127
|
-
// 90–100: interpolate from baseL down to DARK_L
|
|
128
|
-
const t = (step - 80) / 20; // 0 → 1 as step goes 80 → 100
|
|
129
|
-
newL = baseL - t * (baseL - DARK_L);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Reduce chroma slightly at extremes to avoid out-of-gamut clipping
|
|
133
|
-
const chromaScale = 1 - Math.max(0, (newL - 0.90) / 0.07) * 0.5 // reduce near white
|
|
134
|
-
- Math.max(0, (0.25 - newL) / 0.10) * 0.5; // reduce near black
|
|
135
|
-
const newC = baseC * Math.max(0, chromaScale);
|
|
136
|
-
|
|
137
|
-
scale[step] = oklchToHex(newL, newC, baseH);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return scale;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function generateAllColours(colourConfig) {
|
|
144
|
-
const allColours = {};
|
|
145
|
-
|
|
146
|
-
Object.entries(colourConfig).forEach(([name, baseHex]) => {
|
|
147
|
-
allColours[name] = generateColourScale(baseHex);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return allColours;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ============================================================================
|
|
154
|
-
// SPACING SCALE
|
|
155
|
-
// ============================================================================
|
|
156
|
-
|
|
157
|
-
function generateSpacing(baseUnit, scale) {
|
|
158
|
-
// Spacing values are defined explicitly in emily.config.json under spacing.scale.
|
|
159
|
-
// The baseUnit key in config is informational only — it documents the design intent
|
|
160
|
-
// (e.g. "this system is based on 8px") but does not drive generation.
|
|
161
|
-
return scale;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ============================================================================
|
|
165
|
-
// FONT PRESETS
|
|
166
|
-
// ============================================================================
|
|
167
|
-
|
|
168
|
-
// Font presets define the CSS font-family stack only.
|
|
169
|
-
// Loading the actual font files is the user's responsibility — link them in your HTML
|
|
170
|
-
// or use @fontsource packages in your build. emily-css does not generate @import rules
|
|
171
|
-
// for external CDNs so it stays self-contained and works offline.
|
|
172
|
-
// See docs: https://emilyui.com/docs/getting-started
|
|
173
|
-
const FONT_PRESETS = {
|
|
174
|
-
'system': {
|
|
175
|
-
stack: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
176
|
-
},
|
|
177
|
-
'inter': {
|
|
178
|
-
name: 'Inter',
|
|
179
|
-
stack: '"Inter", system-ui, sans-serif',
|
|
180
|
-
},
|
|
181
|
-
'lexend': {
|
|
182
|
-
name: 'Lexend',
|
|
183
|
-
stack: '"Lexend", system-ui, sans-serif',
|
|
184
|
-
},
|
|
185
|
-
'georgia': {
|
|
186
|
-
stack: 'Georgia, "Times New Roman", serif',
|
|
187
|
-
},
|
|
188
|
-
'dm-sans': {
|
|
189
|
-
name: 'DM Sans',
|
|
190
|
-
stack: '"DM Sans", system-ui, sans-serif',
|
|
191
|
-
},
|
|
192
|
-
'nunito': {
|
|
193
|
-
name: 'Nunito',
|
|
194
|
-
stack: '"Nunito", system-ui, sans-serif',
|
|
195
|
-
},
|
|
196
|
-
'atkinson': {
|
|
197
|
-
name: 'Atkinson Hyperlegible',
|
|
198
|
-
stack: '"Atkinson Hyperlegible", system-ui, sans-serif',
|
|
199
|
-
},
|
|
200
|
-
'mono': {
|
|
201
|
-
stack: '"Menlo", "Monaco", "Courier New", monospace',
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
function generateFontCSS(config) {
|
|
206
|
-
// Support both legacy string format and new { heading, body } object format
|
|
207
|
-
const fontConfig = config.fontFamily || 'system';
|
|
208
|
-
let headingKey, bodyKey;
|
|
209
|
-
|
|
210
|
-
if (typeof fontConfig === 'object') {
|
|
211
|
-
headingKey = (fontConfig.heading || 'system').toLowerCase();
|
|
212
|
-
bodyKey = (fontConfig.body || 'system').toLowerCase();
|
|
213
|
-
} else {
|
|
214
|
-
headingKey = fontConfig.toLowerCase();
|
|
215
|
-
bodyKey = fontConfig.toLowerCase();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const headingPreset = FONT_PRESETS[headingKey] || FONT_PRESETS['system'];
|
|
219
|
-
const bodyPreset = FONT_PRESETS[bodyKey] || FONT_PRESETS['system'];
|
|
220
|
-
|
|
221
|
-
let fontFace = '';
|
|
222
|
-
let bodyFont = '';
|
|
223
|
-
|
|
224
|
-
bodyFont += ` body {\n font-family: ${bodyPreset.stack};\n font-synthesis: style;\n }\n`;
|
|
225
|
-
bodyFont += ` h1, h2, h3, h4, h5, h6 {\n font-family: ${headingPreset.stack};\n }\n`;
|
|
226
|
-
|
|
227
|
-
return { fontFace, bodyFont };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ============================================================================
|
|
231
|
-
// CSS VARIABLE GENERATION
|
|
232
|
-
// ============================================================================
|
|
233
|
-
|
|
234
|
-
function generateCSSVariables(colours, spacing, config) {
|
|
235
|
-
let css = `:root {\n`;
|
|
236
|
-
|
|
237
|
-
// Colour variables (full shade scale)
|
|
238
|
-
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
239
|
-
Object.entries(shades).forEach(([shade, hex]) => {
|
|
240
|
-
css += ` --color-${colourName}-${shade}: ${hex};\n`;
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Semantic colour variables (single value, no shade scale)
|
|
245
|
-
if (config.semanticColours) {
|
|
246
|
-
Object.entries(config.semanticColours).forEach(([name, hex]) => {
|
|
247
|
-
css += ` --color-${name}: ${hex};\n`;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
css += ` --focus-ring-glow: color-mix(in srgb, var(--color-brand-80) 12%, transparent);\n`;
|
|
251
|
-
|
|
252
|
-
// Spacing variables
|
|
253
|
-
Object.entries(spacing).forEach(([key, value]) => {
|
|
254
|
-
css += ` --space-${key}: ${value};\n`;
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Font size variables with line-height
|
|
258
|
-
config.typography.fontSizes.forEach(fontSize => {
|
|
259
|
-
const sizeVal = parseInt(fontSize.value);
|
|
260
|
-
css += ` --text-${fontSize.name}: ${fontSize.value};\n`;
|
|
261
|
-
css += ` --leading-${fontSize.name}: ${fontSize.lineHeight};\n`;
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Font weight variables
|
|
265
|
-
Object.entries(config.typography.fontWeights).forEach(([name, weight]) => {
|
|
266
|
-
css += ` --font-weight-${name}: ${weight};\n`;
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Breakpoints
|
|
270
|
-
Object.entries(config.breakpoints).forEach(([name, value]) => {
|
|
271
|
-
css += ` --breakpoint-${name}: ${value};\n`;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Shadows
|
|
275
|
-
Object.entries(config.shadows).forEach(([name, shadow]) => {
|
|
276
|
-
css += ` --shadow-${name}: ${shadow};\n`;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Z-index
|
|
280
|
-
Object.entries(config.zIndex).forEach(([name, value]) => {
|
|
281
|
-
css += ` --z-${name}: ${value};\n`;
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Transitions
|
|
285
|
-
css += ` --transition-duration: ${config.transitions.base};\n`;
|
|
286
|
-
css += ` --transition-timing: ${config.transitions.timing};\n`;
|
|
287
|
-
|
|
288
|
-
css += `}\n\n`;
|
|
289
|
-
return css;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const {
|
|
293
|
-
displayUtilities,
|
|
294
|
-
sizingUtilities,
|
|
295
|
-
positioningUtilities,
|
|
296
|
-
overflowUtilities,
|
|
297
|
-
opacityUtilities,
|
|
298
|
-
transitionUtilities,
|
|
299
|
-
transformUtilities,
|
|
300
|
-
shadowUtilities,
|
|
301
|
-
ringUtilities,
|
|
302
|
-
objectUtilities,
|
|
303
|
-
tableListUtilities,
|
|
304
|
-
svgUtilities,
|
|
305
|
-
formUtilities,
|
|
306
|
-
verticalAlignUtilities,
|
|
307
|
-
contentScrollUtilities,
|
|
308
|
-
blendUtilities,
|
|
309
|
-
cursorUtilities,
|
|
310
|
-
accessibilityUtilities,
|
|
311
|
-
containerUtilities,
|
|
312
|
-
codeUtilities,
|
|
313
|
-
animationUtilities,
|
|
314
|
-
backdropUtilities,
|
|
315
|
-
spaceUtilities,
|
|
316
|
-
divideUtilities,
|
|
317
|
-
backgroundUtilities,
|
|
318
|
-
filterUtilities,
|
|
319
|
-
} = require('./generators');
|
|
320
|
-
|
|
321
|
-
// ============================================================================
|
|
322
|
-
// SPACING UTILITIES
|
|
323
|
-
// ============================================================================
|
|
324
|
-
|
|
325
|
-
function escapeClassName(key) {
|
|
326
|
-
// Escape dots in class names for CSS (e.g., "0.5" becomes "0\.5")
|
|
327
|
-
return key.replace(/\./g, '\\.');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function generateSpacingUtilities(spacing) {
|
|
331
|
-
let css = `/* Spacing: Padding & Margin */\n`;
|
|
332
|
-
|
|
333
|
-
// Padding
|
|
334
|
-
Object.entries(spacing).forEach(([key, value]) => {
|
|
335
|
-
const escaped = escapeClassName(key);
|
|
336
|
-
css += `.p-${escaped} { padding: ${value}; }\n`;
|
|
337
|
-
css += `.px-${escaped} { padding-left: ${value}; padding-right: ${value}; }\n`;
|
|
338
|
-
css += `.py-${escaped} { padding-top: ${value}; padding-bottom: ${value}; }\n`;
|
|
339
|
-
css += `.pt-${escaped} { padding-top: ${value}; }\n`;
|
|
340
|
-
css += `.pr-${escaped} { padding-right: ${value}; }\n`;
|
|
341
|
-
css += `.pb-${escaped} { padding-bottom: ${value}; }\n`;
|
|
342
|
-
css += `.pl-${escaped} { padding-left: ${value}; }\n`;
|
|
343
|
-
css += `.ps-${escaped} { padding-inline-start: ${value}; }\n`;
|
|
344
|
-
css += `.pe-${escaped} { padding-inline-end: ${value}; }\n`;
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Margin
|
|
348
|
-
Object.entries(spacing).forEach(([key, value]) => {
|
|
349
|
-
const escaped = escapeClassName(key);
|
|
350
|
-
css += `.m-${escaped} { margin: ${value}; }\n`;
|
|
351
|
-
css += `.mx-${escaped} { margin-left: ${value}; margin-right: ${value}; }\n`;
|
|
352
|
-
css += `.my-${escaped} { margin-top: ${value}; margin-bottom: ${value}; }\n`;
|
|
353
|
-
css += `.mt-${escaped} { margin-top: ${value}; }\n`;
|
|
354
|
-
css += `.mr-${escaped} { margin-right: ${value}; }\n`;
|
|
355
|
-
css += `.mb-${escaped} { margin-bottom: ${value}; }\n`;
|
|
356
|
-
css += `.ml-${escaped} { margin-left: ${value}; }\n`;
|
|
357
|
-
css += `.ms-${escaped} { margin-inline-start: ${value}; }\n`;
|
|
358
|
-
css += `.me-${escaped} { margin-inline-end: ${value}; }\n`;
|
|
359
|
-
if (value !== '0' && value !== '0px') {
|
|
360
|
-
css += `.-m-${escaped} { margin: -${value}; }\n`;
|
|
361
|
-
css += `.-mx-${escaped} { margin-left: -${value}; margin-right: -${value}; }\n`;
|
|
362
|
-
css += `.-my-${escaped} { margin-top: -${value}; margin-bottom: -${value}; }\n`;
|
|
363
|
-
css += `.-mt-${escaped} { margin-top: -${value}; }\n`;
|
|
364
|
-
css += `.-mr-${escaped} { margin-right: -${value}; }\n`;
|
|
365
|
-
css += `.-mb-${escaped} { margin-bottom: -${value}; }\n`;
|
|
366
|
-
css += `.-ml-${escaped} { margin-left: -${value}; }\n`;
|
|
367
|
-
css += `.-ms-${escaped} { margin-inline-start: -${value}; }\n`;
|
|
368
|
-
css += `.-me-${escaped} { margin-inline-end: -${value}; }\n`;
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Margin auto
|
|
373
|
-
css += `.mx-auto { margin-left: auto; margin-right: auto; }\n`;
|
|
374
|
-
css += `.my-auto { margin-top: auto; margin-bottom: auto; }\n`;
|
|
375
|
-
|
|
376
|
-
css += `\n`;
|
|
377
|
-
return css;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ============================================================================
|
|
381
|
-
// FLEXBOX UTILITIES
|
|
382
|
-
// ============================================================================
|
|
383
|
-
|
|
384
|
-
function generateFlexboxUtilities(spacing) {
|
|
385
|
-
let css = `/* Flexbox */\n`;
|
|
386
|
-
|
|
387
|
-
css += `.inline-flex { display: inline-flex; }\n`;
|
|
388
|
-
|
|
389
|
-
// Direction
|
|
390
|
-
css += `.flex-row { flex-direction: row; }\n`;
|
|
391
|
-
css += `.flex-col { flex-direction: column; }\n`;
|
|
392
|
-
css += `.flex-row-reverse { flex-direction: row-reverse; }\n`;
|
|
393
|
-
css += `.flex-col-reverse { flex-direction: column-reverse; }\n`;
|
|
394
|
-
|
|
395
|
-
// Wrap
|
|
396
|
-
css += `.flex-wrap { flex-wrap: wrap; }\n`;
|
|
397
|
-
css += `.flex-nowrap { flex-wrap: nowrap; }\n`;
|
|
398
|
-
css += `.flex-wrap-reverse { flex-wrap: wrap-reverse; }\n`;
|
|
399
|
-
|
|
400
|
-
// Flex shorthand
|
|
401
|
-
css += `.flex-1 { flex: 1 1 0%; }\n`;
|
|
402
|
-
css += `.flex-auto { flex: 1 1 auto; }\n`;
|
|
403
|
-
css += `.flex-initial { flex: 0 1 auto; }\n`;
|
|
404
|
-
css += `.flex-none { flex: none; }\n`;
|
|
405
|
-
|
|
406
|
-
// Grow/shrink
|
|
407
|
-
css += `.grow { flex-grow: 1; }\n`;
|
|
408
|
-
css += `.grow-0 { flex-grow: 0; }\n`;
|
|
409
|
-
css += `.shrink { flex-shrink: 1; }\n`;
|
|
410
|
-
css += `.shrink-0 { flex-shrink: 0; }\n`;
|
|
411
|
-
|
|
412
|
-
// Flex basis
|
|
413
|
-
css += `.basis-auto { flex-basis: auto; }\n`;
|
|
414
|
-
css += `.basis-full { flex-basis: 100%; }\n`;
|
|
415
|
-
Object.entries(spacing).forEach(([key, value]) => {
|
|
416
|
-
const escaped = escapeClassName(key);
|
|
417
|
-
css += `.basis-${escaped} { flex-basis: ${value}; }\n`;
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const fractions = {
|
|
421
|
-
'1\\/2': '50%', '1\\/3': '33.333333%', '2\\/3': '66.666667%',
|
|
422
|
-
'1\\/4': '25%', '2\\/4': '50%', '3\\/4': '75%',
|
|
423
|
-
'1\\/5': '20%', '2\\/5': '40%', '3\\/5': '60%', '4\\/5': '80%',
|
|
424
|
-
'1\\/6': '16.666667%', '2\\/6': '33.333333%', '3\\/6': '50%', '4\\/6': '66.666667%', '5\\/6': '83.333333%',
|
|
425
|
-
'1\\/12': '8.333333%', '2\\/12': '16.666667%', '3\\/12': '25%', '4\\/12': '33.333333%', '5\\/12': '41.666667%', '6\\/12': '50%', '7\\/12': '58.333333%', '8\\/12': '66.666667%', '9\\/12': '75%', '10\\/12': '83.333333%', '11\\/12': '91.666667%'
|
|
426
|
-
};
|
|
427
|
-
Object.entries(fractions).forEach(([name, value]) => {
|
|
428
|
-
css += `.basis-${name} { flex-basis: ${value}; }\n`;
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Order
|
|
432
|
-
css += `.order-first { order: -9999; }\n`;
|
|
433
|
-
css += `.order-last { order: 9999; }\n`;
|
|
434
|
-
css += `.order-none { order: 0; }\n`;
|
|
435
|
-
for (let i = 1; i <= 12; i++) {
|
|
436
|
-
css += `.order-${i} { order: ${i}; }\n`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Justify (main axis)
|
|
440
|
-
css += `.justify-normal { justify-content: normal; }\n`;
|
|
441
|
-
css += `.justify-start { justify-content: flex-start; }\n`;
|
|
442
|
-
css += `.justify-end { justify-content: flex-end; }\n`;
|
|
443
|
-
css += `.justify-center { justify-content: center; }\n`;
|
|
444
|
-
css += `.justify-between { justify-content: space-between; }\n`;
|
|
445
|
-
css += `.justify-around { justify-content: space-around; }\n`;
|
|
446
|
-
css += `.justify-evenly { justify-content: space-evenly; }\n`;
|
|
447
|
-
css += `.justify-stretch { justify-content: stretch; }\n`;
|
|
448
|
-
|
|
449
|
-
// Content alignment
|
|
450
|
-
css += `.content-normal { align-content: normal; }\n`;
|
|
451
|
-
css += `.content-center { align-content: center; }\n`;
|
|
452
|
-
css += `.content-start { align-content: flex-start; }\n`;
|
|
453
|
-
css += `.content-end { align-content: flex-end; }\n`;
|
|
454
|
-
css += `.content-between { align-content: space-between; }\n`;
|
|
455
|
-
css += `.content-around { align-content: space-around; }\n`;
|
|
456
|
-
css += `.content-evenly { align-content: space-evenly; }\n`;
|
|
457
|
-
css += `.content-baseline { align-content: baseline; }\n`;
|
|
458
|
-
css += `.content-stretch { align-content: stretch; }\n`;
|
|
459
|
-
|
|
460
|
-
// Items (cross axis)
|
|
461
|
-
css += `.items-start { align-items: flex-start; }\n`;
|
|
462
|
-
css += `.items-end { align-items: flex-end; }\n`;
|
|
463
|
-
css += `.items-center { align-items: center; }\n`;
|
|
464
|
-
css += `.items-baseline { align-items: baseline; }\n`;
|
|
465
|
-
css += `.items-stretch { align-items: stretch; }\n`;
|
|
466
|
-
|
|
467
|
-
// Self alignment
|
|
468
|
-
css += `.self-auto { align-self: auto; }\n`;
|
|
469
|
-
css += `.self-start { align-self: flex-start; }\n`;
|
|
470
|
-
css += `.self-end { align-self: flex-end; }\n`;
|
|
471
|
-
css += `.self-center { align-self: center; }\n`;
|
|
472
|
-
css += `.self-stretch { align-self: stretch; }\n`;
|
|
473
|
-
css += `.self-baseline { align-self: baseline; }\n`;
|
|
474
|
-
|
|
475
|
-
// Place utilities
|
|
476
|
-
css += `.place-content-center { place-content: center; }\n`;
|
|
477
|
-
css += `.place-content-start { place-content: start; }\n`;
|
|
478
|
-
css += `.place-content-end { place-content: end; }\n`;
|
|
479
|
-
css += `.place-content-between { place-content: space-between; }\n`;
|
|
480
|
-
css += `.place-content-around { place-content: space-around; }\n`;
|
|
481
|
-
css += `.place-content-evenly { place-content: space-evenly; }\n`;
|
|
482
|
-
css += `.place-content-baseline { place-content: baseline; }\n`;
|
|
483
|
-
css += `.place-content-stretch { place-content: stretch; }\n`;
|
|
484
|
-
css += `.place-items-start { place-items: start; }\n`;
|
|
485
|
-
css += `.place-items-end { place-items: end; }\n`;
|
|
486
|
-
css += `.place-items-center { place-items: center; }\n`;
|
|
487
|
-
css += `.place-items-baseline { place-items: baseline; }\n`;
|
|
488
|
-
css += `.place-items-stretch { place-items: stretch; }\n`;
|
|
489
|
-
css += `.place-self-auto { place-self: auto; }\n`;
|
|
490
|
-
css += `.place-self-start { place-self: start; }\n`;
|
|
491
|
-
css += `.place-self-end { place-self: end; }\n`;
|
|
492
|
-
css += `.place-self-center { place-self: center; }\n`;
|
|
493
|
-
css += `.place-self-stretch { place-self: stretch; }\n`;
|
|
494
|
-
|
|
495
|
-
css += `\n`;
|
|
496
|
-
return css;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ============================================================================
|
|
500
|
-
// GRID UTILITIES
|
|
501
|
-
// ============================================================================
|
|
502
|
-
|
|
503
|
-
function generateGridUtilities(spacing) {
|
|
504
|
-
let css = `/* Grid */\n`;
|
|
505
|
-
|
|
506
|
-
css += `.inline-grid { display: inline-grid; }\n`;
|
|
507
|
-
|
|
508
|
-
css += `.grid-cols-none { grid-template-columns: none; }\n`;
|
|
509
|
-
css += `.grid-cols-subgrid { grid-template-columns: subgrid; }\n`;
|
|
510
|
-
for (let i = 1; i <= 12; i++) {
|
|
511
|
-
css += `.grid-cols-${i} { grid-template-columns: repeat(${i}, minmax(0, 1fr)); }\n`;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
css += `.grid-rows-none { grid-template-rows: none; }\n`;
|
|
515
|
-
css += `.grid-rows-subgrid { grid-template-rows: subgrid; }\n`;
|
|
516
|
-
for (let i = 1; i <= 12; i++) {
|
|
517
|
-
css += `.grid-rows-${i} { grid-template-rows: repeat(${i}, minmax(0, 1fr)); }\n`;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
for (let i = 1; i <= 12; i++) {
|
|
521
|
-
css += `.col-span-${i} { grid-column: span ${i} / span ${i}; }\n`;
|
|
522
|
-
}
|
|
523
|
-
css += `.col-span-full { grid-column: 1 / -1; }\n`;
|
|
524
|
-
css += `.col-auto { grid-column: auto; }\n`;
|
|
525
|
-
for (let i = 1; i <= 13; i++) {
|
|
526
|
-
css += `.col-start-${i} { grid-column-start: ${i}; }\n`;
|
|
527
|
-
css += `.col-end-${i} { grid-column-end: ${i}; }\n`;
|
|
528
|
-
}
|
|
529
|
-
css += `.col-start-auto { grid-column-start: auto; }\n`;
|
|
530
|
-
css += `.col-end-auto { grid-column-end: auto; }\n`;
|
|
531
|
-
|
|
532
|
-
for (let i = 1; i <= 12; i++) {
|
|
533
|
-
css += `.row-span-${i} { grid-row: span ${i} / span ${i}; }\n`;
|
|
534
|
-
}
|
|
535
|
-
css += `.row-span-full { grid-row: 1 / -1; }\n`;
|
|
536
|
-
css += `.row-auto { grid-row: auto; }\n`;
|
|
537
|
-
for (let i = 1; i <= 13; i++) {
|
|
538
|
-
css += `.row-start-${i} { grid-row-start: ${i}; }\n`;
|
|
539
|
-
css += `.row-end-${i} { grid-row-end: ${i}; }\n`;
|
|
540
|
-
}
|
|
541
|
-
css += `.row-start-auto { grid-row-start: auto; }\n`;
|
|
542
|
-
css += `.row-end-auto { grid-row-end: auto; }\n`;
|
|
543
|
-
|
|
544
|
-
css += `.grid-flow-row { grid-auto-flow: row; }\n`;
|
|
545
|
-
css += `.grid-flow-col { grid-auto-flow: column; }\n`;
|
|
546
|
-
css += `.grid-flow-dense { grid-auto-flow: dense; }\n`;
|
|
547
|
-
css += `.grid-flow-row-dense { grid-auto-flow: row dense; }\n`;
|
|
548
|
-
css += `.grid-flow-col-dense { grid-auto-flow: column dense; }\n`;
|
|
549
|
-
|
|
550
|
-
css += `.auto-cols-auto { grid-auto-columns: auto; }\n`;
|
|
551
|
-
css += `.auto-cols-min { grid-auto-columns: min-content; }\n`;
|
|
552
|
-
css += `.auto-cols-max { grid-auto-columns: max-content; }\n`;
|
|
553
|
-
css += `.auto-cols-fr { grid-auto-columns: minmax(0, 1fr); }\n`;
|
|
554
|
-
css += `.auto-rows-auto { grid-auto-rows: auto; }\n`;
|
|
555
|
-
css += `.auto-rows-min { grid-auto-rows: min-content; }\n`;
|
|
556
|
-
css += `.auto-rows-max { grid-auto-rows: max-content; }\n`;
|
|
557
|
-
css += `.auto-rows-fr { grid-auto-rows: minmax(0, 1fr); }\n`;
|
|
558
|
-
|
|
559
|
-
Object.entries(spacing).forEach(([key, value]) => {
|
|
560
|
-
const escaped = escapeClassName(key);
|
|
561
|
-
css += `.gap-${escaped} { gap: ${value}; }\n`;
|
|
562
|
-
css += `.gap-x-${escaped} { column-gap: ${value}; }\n`;
|
|
563
|
-
css += `.gap-y-${escaped} { row-gap: ${value}; }\n`;
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
css += `.justify-items-start { justify-items: start; }\n`;
|
|
567
|
-
css += `.justify-items-end { justify-items: end; }\n`;
|
|
568
|
-
css += `.justify-items-center { justify-items: center; }\n`;
|
|
569
|
-
css += `.justify-items-stretch { justify-items: stretch; }\n`;
|
|
570
|
-
css += `.justify-self-auto { justify-self: auto; }\n`;
|
|
571
|
-
css += `.justify-self-start { justify-self: start; }\n`;
|
|
572
|
-
css += `.justify-self-end { justify-self: end; }\n`;
|
|
573
|
-
css += `.justify-self-center { justify-self: center; }\n`;
|
|
574
|
-
css += `.justify-self-stretch { justify-self: stretch; }\n`;
|
|
575
|
-
|
|
576
|
-
css += `\n`;
|
|
577
|
-
return css;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// ============================================================================
|
|
581
|
-
// TYPOGRAPHY UTILITIES
|
|
582
|
-
// ============================================================================
|
|
583
|
-
|
|
584
|
-
function generateTypographyUtilities(config) {
|
|
585
|
-
let css = `/* Typography */\n`;
|
|
586
|
-
|
|
587
|
-
config.typography.fontSizes.forEach(fontSize => {
|
|
588
|
-
css += `.text-${fontSize.name} { font-size: var(--text-${fontSize.name}); line-height: ${fontSize.lineHeight}; }\n`;
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
const fontWeightDefaults = {
|
|
592
|
-
thin: 100,
|
|
593
|
-
extralight: 200,
|
|
594
|
-
light: 300,
|
|
595
|
-
normal: 400,
|
|
596
|
-
medium: 500,
|
|
597
|
-
semibold: 600,
|
|
598
|
-
bold: 700,
|
|
599
|
-
extrabold: 800,
|
|
600
|
-
black: 900,
|
|
601
|
-
};
|
|
602
|
-
const resolvedFontWeights = {
|
|
603
|
-
...fontWeightDefaults,
|
|
604
|
-
...(config.typography.fontWeights || {}),
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
Object.entries(resolvedFontWeights).forEach(([name, weight]) => {
|
|
608
|
-
css += `.font-${name} { font-weight: ${weight}; }\n`;
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
css += `.italic { font-style: italic; }\n`;
|
|
612
|
-
css += `.not-italic { font-style: normal; }\n`;
|
|
613
|
-
|
|
614
|
-
css += `.text-left { text-align: left; }\n`;
|
|
615
|
-
css += `.text-center { text-align: center; }\n`;
|
|
616
|
-
css += `.text-right { text-align: right; }\n`;
|
|
617
|
-
css += `.text-justify { text-align: justify; }\n`;
|
|
618
|
-
css += `.text-start { text-align: start; }\n`;
|
|
619
|
-
css += `.text-end { text-align: end; }\n`;
|
|
620
|
-
|
|
621
|
-
css += `.whitespace-normal { white-space: normal; }\n`;
|
|
622
|
-
css += `.whitespace-nowrap { white-space: nowrap; }\n`;
|
|
623
|
-
css += `.whitespace-pre { white-space: pre; }\n`;
|
|
624
|
-
css += `.whitespace-pre-line { white-space: pre-line; }\n`;
|
|
625
|
-
css += `.whitespace-pre-wrap { white-space: pre-wrap; }\n`;
|
|
626
|
-
css += `.whitespace-break-spaces { white-space: break-spaces; }\n`;
|
|
627
|
-
css += `.text-wrap { text-wrap: wrap; }\n`;
|
|
628
|
-
css += `.text-nowrap { text-wrap: nowrap; }\n`;
|
|
629
|
-
css += `.text-balance { text-wrap: balance; }\n`;
|
|
630
|
-
css += `.text-pretty { text-wrap: pretty; }\n`;
|
|
631
|
-
css += `.break-normal { overflow-wrap: normal; word-break: normal; }\n`;
|
|
632
|
-
css += `.break-words { overflow-wrap: break-word; }\n`;
|
|
633
|
-
css += `.break-all { word-break: break-all; }\n`;
|
|
634
|
-
css += `.break-keep { word-break: keep-all; }\n`;
|
|
635
|
-
css += `.hyphens-none { hyphens: none; }\n`;
|
|
636
|
-
css += `.hyphens-manual { hyphens: manual; }\n`;
|
|
637
|
-
css += `.hyphens-auto { hyphens: auto; }\n`;
|
|
638
|
-
|
|
639
|
-
css += `.leading-none { line-height: 1; }\n`;
|
|
640
|
-
css += `.leading-tight { line-height: 1.25; }\n`;
|
|
641
|
-
css += `.leading-snug { line-height: 1.375; }\n`;
|
|
642
|
-
css += `.leading-normal { line-height: 1.5; }\n`;
|
|
643
|
-
css += `.leading-relaxed { line-height: 1.625; }\n`;
|
|
644
|
-
css += `.leading-loose { line-height: 2; }\n`;
|
|
645
|
-
css += `.text-display { font-size: clamp(2.5rem, 6vw, 4rem); }\n`;
|
|
646
|
-
|
|
647
|
-
css += `.tracking-tighter { letter-spacing: -0.05em; }\n`;
|
|
648
|
-
css += `.tracking-tight { letter-spacing: -0.025em; }\n`;
|
|
649
|
-
css += `.tracking-normal { letter-spacing: 0em; }\n`;
|
|
650
|
-
css += `.tracking-wide { letter-spacing: 0.025em; }\n`;
|
|
651
|
-
css += `.tracking-wider { letter-spacing: 0.05em; }\n`;
|
|
652
|
-
css += `.tracking-widest { letter-spacing: 0.1em; }\n`;
|
|
653
|
-
|
|
654
|
-
css += `.underline { text-decoration-line: underline; }\n`;
|
|
655
|
-
css += `.overline { text-decoration-line: overline; }\n`;
|
|
656
|
-
css += `.line-through { text-decoration-line: line-through; }\n`;
|
|
657
|
-
css += `.no-underline { text-decoration-line: none; }\n`;
|
|
658
|
-
css += `.decoration-solid { text-decoration-style: solid; }\n`;
|
|
659
|
-
css += `.decoration-double { text-decoration-style: double; }\n`;
|
|
660
|
-
css += `.decoration-dotted { text-decoration-style: dotted; }\n`;
|
|
661
|
-
css += `.decoration-dashed { text-decoration-style: dashed; }\n`;
|
|
662
|
-
css += `.decoration-wavy { text-decoration-style: wavy; }\n`;
|
|
663
|
-
css += `.underline-offset-auto { text-underline-offset: auto; }\n`;
|
|
664
|
-
[0, 1, 2, 4, 8].forEach(value => {
|
|
665
|
-
css += `.underline-offset-${value} { text-underline-offset: ${value}px; }\n`;
|
|
666
|
-
});
|
|
667
|
-
css += `.decoration-auto { text-decoration-thickness: auto; }\n`;
|
|
668
|
-
css += `.decoration-from-font { text-decoration-thickness: from-font; }\n`;
|
|
669
|
-
[0, 1, 2, 4, 8].forEach(value => {
|
|
670
|
-
css += `.decoration-${value} { text-decoration-thickness: ${value}px; }\n`;
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
css += `.normal-nums { font-variant-numeric: normal; }\n`;
|
|
674
|
-
css += `.ordinal { font-variant-numeric: ordinal; }\n`;
|
|
675
|
-
css += `.slashed-zero { font-variant-numeric: slashed-zero; }\n`;
|
|
676
|
-
css += `.lining-nums { font-variant-numeric: lining-nums; }\n`;
|
|
677
|
-
css += `.oldstyle-nums { font-variant-numeric: oldstyle-nums; }\n`;
|
|
678
|
-
css += `.proportional-nums { font-variant-numeric: proportional-nums; }\n`;
|
|
679
|
-
css += `.tabular-nums { font-variant-numeric: tabular-nums; }\n`;
|
|
680
|
-
css += `.diagonal-fractions { font-variant-numeric: diagonal-fractions; }\n`;
|
|
681
|
-
css += `.stacked-fractions { font-variant-numeric: stacked-fractions; }\n`;
|
|
682
|
-
|
|
683
|
-
css += `.uppercase { text-transform: uppercase; }\n`;
|
|
684
|
-
css += `.lowercase { text-transform: lowercase; }\n`;
|
|
685
|
-
css += `.capitalize { text-transform: capitalize; }\n`;
|
|
686
|
-
css += `.normal-case { text-transform: none; }\n`;
|
|
687
|
-
|
|
688
|
-
css += `.font-sans { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n`;
|
|
689
|
-
css += `.font-serif { font-family: Georgia, "Times New Roman", serif; }\n`;
|
|
690
|
-
css += `.font-mono { font-family: "Menlo", "Monaco", "Courier New", monospace; }\n`;
|
|
691
|
-
css += `.font-inter { font-family: "Inter", system-ui, sans-serif; }\n`;
|
|
692
|
-
css += `.font-lexend { font-family: "Lexend", system-ui, sans-serif; }\n`;
|
|
693
|
-
css += `.font-dm-sans { font-family: "DM Sans", system-ui, sans-serif; }\n`;
|
|
694
|
-
css += `.font-nunito { font-family: "Nunito", system-ui, sans-serif; }\n`;
|
|
695
|
-
css += `.font-atkinson { font-family: "Atkinson Hyperlegible", system-ui, sans-serif; }\n`;
|
|
696
|
-
|
|
697
|
-
css += `\n`;
|
|
698
|
-
return css;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ============================================================================
|
|
702
|
-
// BORDER UTILITIES
|
|
703
|
-
// ============================================================================
|
|
704
|
-
|
|
705
|
-
function generateBorderUtilities(config) {
|
|
706
|
-
let css = `/* Borders & Radius */\n`;
|
|
707
|
-
|
|
708
|
-
const borderWidths = config.spacing.borderWidths || [0, 2, 4, 8];
|
|
709
|
-
|
|
710
|
-
css += `.border { border-width: 1px; border-style: solid; }\n`;
|
|
711
|
-
borderWidths.forEach(width => {
|
|
712
|
-
css += `.border-${width} { border-width: ${width}px; }\n`;
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
const sides = {
|
|
716
|
-
t: 'top', r: 'right', b: 'bottom', l: 'left',
|
|
717
|
-
x: ['left', 'right'], y: ['top', 'bottom'],
|
|
718
|
-
s: 'inline-start', e: 'inline-end'
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
Object.entries(sides).forEach(([side, value]) => {
|
|
722
|
-
if (Array.isArray(value)) {
|
|
723
|
-
css += `.border-${side} { border-${value[0]}-width: 1px; border-${value[1]}-width: 1px; border-${value[0]}-style: solid; border-${value[1]}-style: solid; }\n`;
|
|
724
|
-
} else {
|
|
725
|
-
css += `.border-${side} { border-${value}-width: 1px; border-${value}-style: solid; }\n`;
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
borderWidths.forEach(width => {
|
|
730
|
-
Object.entries(sides).forEach(([side, value]) => {
|
|
731
|
-
if (Array.isArray(value)) {
|
|
732
|
-
css += `.border-${side}-${width} { border-${value[0]}-width: ${width}px; border-${value[1]}-width: ${width}px; border-${value[0]}-style: solid; border-${value[1]}-style: solid; }\n`;
|
|
733
|
-
} else {
|
|
734
|
-
css += `.border-${side}-${width} { border-${value}-width: ${width}px; border-${value}-style: solid; }\n`;
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
css += `.border-solid { border-style: solid; }\n`;
|
|
740
|
-
css += `.border-dashed { border-style: dashed; }\n`;
|
|
741
|
-
css += `.border-dotted { border-style: dotted; }\n`;
|
|
742
|
-
css += `.border-double { border-style: double; }\n`;
|
|
743
|
-
css += `.border-hidden { border-style: hidden; }\n`;
|
|
744
|
-
css += `.border-none { border-style: none; }\n`;
|
|
745
|
-
css += `.border-transparent { border-color: transparent; }\n`;
|
|
746
|
-
css += `.border-current { border-color: currentColor; }\n`;
|
|
747
|
-
css += `.border-black { border-color: #111110; }\n`;
|
|
748
|
-
css += `.border-white { border-color: #FAFAFA; }\n`;
|
|
749
|
-
|
|
750
|
-
const baseRadius = config.spacing.borderRadius['base'] || '8px';
|
|
751
|
-
css += `.rounded { border-radius: ${baseRadius}; }\n`;
|
|
752
|
-
|
|
753
|
-
Object.entries(config.spacing.borderRadius).forEach(([name, value]) => {
|
|
754
|
-
css += `.rounded-${name} { border-radius: ${value}; }\n`;
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
const radiusTargets = {
|
|
758
|
-
t: ['top-left', 'top-right'], r: ['top-right', 'bottom-right'],
|
|
759
|
-
b: ['bottom-right', 'bottom-left'], l: ['top-left', 'bottom-left'],
|
|
760
|
-
tl: ['top-left'], tr: ['top-right'], br: ['bottom-right'], bl: ['bottom-left']
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
Object.entries(radiusTargets).forEach(([side, corners]) => {
|
|
764
|
-
corners.forEach(corner => {
|
|
765
|
-
css += `.rounded-${side} { border-${corner}-radius: ${baseRadius}; }\n`;
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
Object.entries(config.spacing.borderRadius).forEach(([name, value]) => {
|
|
770
|
-
Object.entries(radiusTargets).forEach(([side, corners]) => {
|
|
771
|
-
corners.forEach(corner => {
|
|
772
|
-
css += `.rounded-${side}-${name} { border-${corner}-radius: ${value}; }\n`;
|
|
773
|
-
});
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
css += `\n`;
|
|
778
|
-
return css;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// ============================================================================
|
|
782
|
-
// BASE ELEMENT STYLES
|
|
783
|
-
// ============================================================================
|
|
784
|
-
|
|
785
|
-
function generateBaseStyles(config) {
|
|
786
|
-
const baseStyles = config.baseStyles;
|
|
787
|
-
if (!baseStyles || Object.keys(baseStyles).length === 0) return '';
|
|
788
|
-
|
|
789
|
-
// Build a lookup map from font size name → CSS variable
|
|
790
|
-
const fontSizeMap = {};
|
|
791
|
-
(config.typography?.fontSizes || []).forEach(({ name }) => {
|
|
792
|
-
fontSizeMap[name] = `var(--text-${name})`;
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Line height hints per size — keeps headings tighter than body text
|
|
796
|
-
const lineHeightMap = {
|
|
797
|
-
xs: '1.5', sm: '1.5', base: '1.6', lg: '1.6',
|
|
798
|
-
xl: '1.5', '2xl': '1.4', '3xl': '1.35', '4xl': '1.3',
|
|
799
|
-
'5xl': '1.15', '6xl': '1.1', '7xl': '1.05', '8xl': '1', '9xl': '1'
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
let css = '\n /* Base element styles (from baseStyles in emily.config.json) */\n';
|
|
803
|
-
Object.entries(baseStyles).forEach(([element, sizeKey]) => {
|
|
804
|
-
const varRef = fontSizeMap[sizeKey];
|
|
805
|
-
if (!varRef) return;
|
|
806
|
-
const lh = lineHeightMap[sizeKey] || '1.5';
|
|
807
|
-
css += ` ${element} { font-size: ${varRef}; line-height: ${lh}; }\n`;
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
return css;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ============================================================================
|
|
814
|
-
// COLOUR UTILITIES
|
|
815
|
-
// ============================================================================
|
|
816
|
-
|
|
817
|
-
function generateColourUtilities(colours) {
|
|
818
|
-
let css = `/* Colours: Background, Text, Borders, Accents */\n`;
|
|
819
|
-
// Uses CSS custom properties rather than hardcoded hex so colour utilities
|
|
820
|
-
// can be overridden via variable redefinition (e.g. dark mode, theme layers).
|
|
821
|
-
// The hex values are still declared as --color-* tokens in @layer theme.
|
|
822
|
-
|
|
823
|
-
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
824
|
-
// Background colours
|
|
825
|
-
Object.entries(shades).forEach(([shade]) => {
|
|
826
|
-
css += `.bg-${colourName}-${shade} { background-color: var(--color-${colourName}-${shade}); }\n`;
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
// Text colours
|
|
830
|
-
Object.entries(shades).forEach(([shade]) => {
|
|
831
|
-
css += `.text-${colourName}-${shade} { color: var(--color-${colourName}-${shade}); }\n`;
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// Border colours
|
|
835
|
-
Object.entries(shades).forEach(([shade]) => {
|
|
836
|
-
css += `.border-${colourName}-${shade} { border-color: var(--color-${colourName}-${shade}); }\n`;
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// Accent colours (for form elements like checkboxes, radio buttons)
|
|
840
|
-
Object.entries(shades).forEach(([shade]) => {
|
|
841
|
-
css += `.accent-${colourName}-${shade} { accent-color: var(--color-${colourName}-${shade}); }\n`;
|
|
842
|
-
});
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
css += `.bg-white { background-color: #FAFAFA; }\n`;
|
|
846
|
-
css += `.bg-black { background-color: #111110; }\n`;
|
|
847
|
-
css += `.bg-transparent { background-color: transparent; }\n`;
|
|
848
|
-
css += `.bg-current { background-color: currentColor; }\n`;
|
|
849
|
-
|
|
850
|
-
css += `.text-white { color: #FAFAFA; }\n`;
|
|
851
|
-
css += `.text-black { color: #111110; }\n`;
|
|
852
|
-
css += `.text-transparent { color: transparent; }\n`;
|
|
853
|
-
css += `.text-current { color: currentColor; }\n`;
|
|
854
|
-
|
|
855
|
-
css += `\n`;
|
|
856
|
-
return css;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function generateSemanticColourUtilities(semanticColours) {
|
|
860
|
-
if (!semanticColours) return '';
|
|
861
|
-
let css = `/* Semantic colours: single value, no shade scale */\n`;
|
|
862
|
-
Object.entries(semanticColours).forEach(([name]) => {
|
|
863
|
-
css += `.bg-${name} { background-color: var(--color-${name}); }\n`;
|
|
864
|
-
css += `.text-${name} { color: var(--color-${name}); }\n`;
|
|
865
|
-
css += `.border-${name} { border-color: var(--color-${name}); }\n`;
|
|
866
|
-
css += `.fill-${name} { fill: var(--color-${name}); }\n`;
|
|
867
|
-
});
|
|
868
|
-
css += `\n`;
|
|
869
|
-
return css;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// ============================================================================
|
|
873
|
-
// ARIA & DATA-STATE VARIANTS
|
|
874
|
-
// ============================================================================
|
|
875
|
-
// Generates ARIA attribute and data-state variants for all utility classes.
|
|
876
|
-
// Selectors target the attribute value directly so they work without JS —
|
|
877
|
-
// just toggle the attribute and the utility activates.
|
|
878
|
-
//
|
|
879
|
-
// Usage in HTML:
|
|
880
|
-
// aria-expanded: class="aria-expanded:block" aria-expanded="true"
|
|
881
|
-
// data-open: class="data-open:flex" data-state="open"
|
|
882
|
-
//
|
|
883
|
-
// Output examples:
|
|
884
|
-
// .aria-expanded\:block[aria-expanded="true"] { display: block; }
|
|
885
|
-
// .data-open\:flex[data-state="open"] { display: flex; }
|
|
886
|
-
|
|
887
|
-
function addAriaDataVariants(css) {
|
|
888
|
-
const variants = [
|
|
889
|
-
{ name: 'aria-expanded', selector: '[aria-expanded="true"]' },
|
|
890
|
-
{ name: 'aria-selected', selector: '[aria-selected="true"]' },
|
|
891
|
-
{ name: 'aria-checked', selector: '[aria-checked="true"]' },
|
|
892
|
-
{ name: 'aria-current', selector: '[aria-current="page"]' },
|
|
893
|
-
{ name: 'aria-disabled', selector: '[aria-disabled="true"]' },
|
|
894
|
-
{ name: 'data-open', selector: '[data-state="open"]' },
|
|
895
|
-
{ name: 'data-closed', selector: '[data-state="closed"]' },
|
|
896
|
-
{ name: 'data-checked', selector: '[data-state="checked"]' },
|
|
897
|
-
{ name: 'data-unchecked', selector: '[data-state="unchecked"]' },
|
|
898
|
-
{ name: 'data-active', selector: '[data-state="active"]' },
|
|
899
|
-
{ name: 'data-inactive', selector: '[data-state="inactive"]' },
|
|
900
|
-
];
|
|
901
|
-
|
|
902
|
-
let variantCss = css;
|
|
903
|
-
|
|
904
|
-
variants.forEach(variant => {
|
|
905
|
-
let variantRules = '';
|
|
906
|
-
const lines = css.split('\n');
|
|
907
|
-
|
|
908
|
-
lines.forEach(line => {
|
|
909
|
-
if (line.startsWith('.') && line.includes('{')) {
|
|
910
|
-
const className = line.split('{')[0].trim();
|
|
911
|
-
// Skip already-variant lines (contain ':' in class name) and special selectors
|
|
912
|
-
if (
|
|
913
|
-
!className.startsWith(':root') &&
|
|
914
|
-
!className.includes('@') &&
|
|
915
|
-
!className.includes('::') &&
|
|
916
|
-
!className.includes(':')
|
|
917
|
-
) {
|
|
918
|
-
const classWithoutDot = className.substring(1);
|
|
919
|
-
const ariaSelector = `.${variant.name}\\:${classWithoutDot}${variant.selector}`;
|
|
920
|
-
const ariaRule = line.replace(className, ariaSelector);
|
|
921
|
-
variantRules += ariaRule + '\n';
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
if (variantRules) {
|
|
927
|
-
variantCss += `\n/* ARIA/data-state variant: ${variant.name} */\n` + variantRules;
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
return variantCss;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// ============================================================================
|
|
935
|
-
// DARK MODE VARIANTS
|
|
936
|
-
// ============================================================================
|
|
937
|
-
// Generates dark: prefixed versions of colour and appearance utilities only.
|
|
938
|
-
// Layout, spacing, and typography utilities don't change in dark mode —
|
|
939
|
-
// targeting only the utilities where dark mode actually makes a difference
|
|
940
|
-
// keeps the output lean and the purge step effective.
|
|
941
|
-
//
|
|
942
|
-
// Usage in HTML: class="bg-neutral-10 dark:bg-neutral-90 text-neutral-90 dark:text-neutral-10"
|
|
943
|
-
// Output: @media (prefers-color-scheme: dark) { .dark\:bg-neutral-90 { background-color: ...; } }
|
|
944
|
-
|
|
945
|
-
function addDarkModeVariants(css) {
|
|
946
|
-
// Match on CSS property, not class name prefix — avoids catching
|
|
947
|
-
// structural utilities like text-xs (font-size) or text-left (text-align)
|
|
948
|
-
// when we only want colour-related declarations.
|
|
949
|
-
const colourProperties = [
|
|
950
|
-
'background-color',
|
|
951
|
-
'color',
|
|
952
|
-
'border-color',
|
|
953
|
-
'accent-color',
|
|
954
|
-
'box-shadow',
|
|
955
|
-
'opacity',
|
|
956
|
-
'fill',
|
|
957
|
-
'stroke',
|
|
958
|
-
'--tw-ring-color',
|
|
959
|
-
'outline-color',
|
|
960
|
-
];
|
|
961
|
-
|
|
962
|
-
let darkRules = '';
|
|
963
|
-
const lines = css.split('\n');
|
|
964
|
-
|
|
965
|
-
lines.forEach(line => {
|
|
966
|
-
if (line.startsWith('.') && line.includes('{') && line.includes('}')) {
|
|
967
|
-
const className = line.split('{')[0].trim();
|
|
968
|
-
|
|
969
|
-
// Only base utilities — skip anything already a variant (contains ':')
|
|
970
|
-
if (className.includes(':')) return;
|
|
971
|
-
|
|
972
|
-
// Only colour/appearance properties
|
|
973
|
-
const isColourUtility = colourProperties.some(prop => line.includes(prop + ':'));
|
|
974
|
-
if (!isColourUtility) return;
|
|
975
|
-
|
|
976
|
-
const classWithoutDot = className.substring(1);
|
|
977
|
-
const darkRule = line.replace(className, `.dark\\:${classWithoutDot}`);
|
|
978
|
-
darkRules += ' ' + darkRule + '\n';
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
if (!darkRules) return css;
|
|
983
|
-
|
|
984
|
-
return css
|
|
985
|
-
+ `\n/* Dark mode variants — explicit override */\n[data-theme="dark"] {\n${darkRules}}\n`
|
|
986
|
-
+ `\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`;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// ============================================================================
|
|
990
|
-
// RESPONSIVE VARIANTS
|
|
991
|
-
// ============================================================================
|
|
992
|
-
|
|
993
|
-
function addResponsiveVariants(css, config) {
|
|
994
|
-
let variantCss = css;
|
|
995
|
-
|
|
996
|
-
Object.entries(config.breakpoints).forEach(([breakpointName, breakpointValue]) => {
|
|
997
|
-
const mediaQuery = `@media (min-width: ${breakpointValue}) {\n`;
|
|
998
|
-
let breakpointRules = '';
|
|
999
|
-
|
|
1000
|
-
// Extract all utility rules and add responsive prefix
|
|
1001
|
-
const lines = css.split('\n');
|
|
1002
|
-
lines.forEach(line => {
|
|
1003
|
-
if (line.startsWith('.') && line.includes('{')) {
|
|
1004
|
-
const className = line.split('{')[0].trim();
|
|
1005
|
-
const rule = line;
|
|
1006
|
-
// Skip variables and already responsive selectors
|
|
1007
|
-
if (!className.startsWith(':root') && !className.includes(':')) {
|
|
1008
|
-
const responsiveRule = rule.replace(className, `.${breakpointName}\\:${className.substring(1)}`);
|
|
1009
|
-
breakpointRules += ' ' + responsiveRule + '\n';
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
if (breakpointRules) {
|
|
1015
|
-
variantCss += mediaQuery + breakpointRules + '}\n\n';
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
return variantCss;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// ============================================================================
|
|
1023
|
-
// STATE VARIANTS
|
|
1024
|
-
// ============================================================================
|
|
1025
|
-
// Add pseudo-class variants for hover, focus-visible, active, disabled
|
|
1026
|
-
|
|
1027
|
-
function addStateVariants(css) {
|
|
1028
|
-
const states = [
|
|
1029
|
-
{ name: 'hover', selector: ':hover' },
|
|
1030
|
-
{ name: 'focus', selector: ':focus' },
|
|
1031
|
-
{ name: 'focus-within', selector: ':focus-within' },
|
|
1032
|
-
{ name: 'focus-visible', selector: ':focus-visible' },
|
|
1033
|
-
{ name: 'active', selector: ':active' },
|
|
1034
|
-
{ name: 'disabled', selector: ':disabled' }
|
|
1035
|
-
];
|
|
1036
|
-
|
|
1037
|
-
let variantCss = css;
|
|
1038
|
-
|
|
1039
|
-
states.forEach(state => {
|
|
1040
|
-
let stateRules = '';
|
|
1041
|
-
|
|
1042
|
-
// Extract all utility rules and add state prefix
|
|
1043
|
-
const lines = css.split('\n');
|
|
1044
|
-
lines.forEach(line => {
|
|
1045
|
-
if (line.startsWith('.') && line.includes('{')) {
|
|
1046
|
-
const className = line.split('{')[0].trim();
|
|
1047
|
-
// Skip variables, media queries, pseudo-elements, and state variants
|
|
1048
|
-
if (!className.startsWith(':root') && !className.includes('@') && !className.includes('::') && !className.includes(':')) {
|
|
1049
|
-
// Generate state variant: .hover\:block:hover { display: block; }
|
|
1050
|
-
// Remove leading dot from className, add state prefix with escaped colon
|
|
1051
|
-
const classWithoutDot = className.substring(1);
|
|
1052
|
-
const stateSelector = `.${state.name}\\:${classWithoutDot}${state.selector}`;
|
|
1053
|
-
const statefulRule = line.replace(className, stateSelector);
|
|
1054
|
-
stateRules += statefulRule + '\n';
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
if (stateRules) {
|
|
1060
|
-
variantCss += '\n/* State variant: ' + state.name + ' */\n' + stateRules;
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
return variantCss;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
// ============================================================================
|
|
1069
|
-
// PATTERN COMPONENTS
|
|
1070
|
-
// ============================================================================
|
|
1071
|
-
// Composite classes that combine multiple utilities into named patterns.
|
|
1072
|
-
// These live in @layer components so utilities always take precedence in the cascade.
|
|
1073
|
-
// Gap values reference spacing variables generated from emily.config.json,
|
|
1074
|
-
// with pixel fallbacks so they work even without the variables in scope.
|
|
1075
|
-
|
|
1076
|
-
function generatePatternComponents() {
|
|
1077
|
-
return `
|
|
1078
|
-
/* ---- Centering ---- */
|
|
1079
|
-
|
|
1080
|
-
/* Full-viewport overlay centering — use for modals, lightboxes, toasts */
|
|
1081
|
-
.center-screen {
|
|
1082
|
-
position: fixed;
|
|
1083
|
-
inset: 0;
|
|
1084
|
-
display: flex;
|
|
1085
|
-
align-items: center;
|
|
1086
|
-
justify-content: center;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/* Transform-based centering within a relative/absolute parent */
|
|
1090
|
-
.center-absolute {
|
|
1091
|
-
position: absolute;
|
|
1092
|
-
top: 50%;
|
|
1093
|
-
left: 50%;
|
|
1094
|
-
transform: translate(-50%, -50%);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
/* ---- Reading / Prose ---- */
|
|
1098
|
-
|
|
1099
|
-
/* Comfortable reading column — limits line length, centers the block */
|
|
1100
|
-
.prose {
|
|
1101
|
-
max-width: 65ch;
|
|
1102
|
-
margin-inline: auto;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
.prose-emily {
|
|
1106
|
-
max-width: 65ch;
|
|
1107
|
-
margin-inline: auto;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
.prose-emily > * + * {
|
|
1111
|
-
margin-top: var(--space-4, 1rem);
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
.prose-emily h2,
|
|
1115
|
-
.prose-emily h3 {
|
|
1116
|
-
font-family: inherit;
|
|
1117
|
-
color: var(--color-neutral-90);
|
|
1118
|
-
line-height: 1.25;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
.prose-emily h2 {
|
|
1122
|
-
font-size: var(--text-2xl, 24px);
|
|
1123
|
-
margin-top: var(--space-10, 2.5rem);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
.prose-emily h3 {
|
|
1127
|
-
font-size: var(--text-xl, 20px);
|
|
1128
|
-
margin-top: var(--space-8, 2rem);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
.prose-emily p,
|
|
1132
|
-
.prose-emily li {
|
|
1133
|
-
color: var(--color-neutral-70);
|
|
1134
|
-
line-height: 1.75;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
.prose-emily ul,
|
|
1138
|
-
.prose-emily ol {
|
|
1139
|
-
padding-left: var(--space-6, 1.5rem);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
.prose-emily ul {
|
|
1143
|
-
list-style-type: disc;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
.prose-emily ol {
|
|
1147
|
-
list-style-type: decimal;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
.prose-emily a {
|
|
1151
|
-
color: var(--color-brand-80);
|
|
1152
|
-
text-decoration: underline;
|
|
1153
|
-
text-underline-offset: 2px;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
.prose-emily code {
|
|
1157
|
-
font-size: var(--text-sm, 14px);
|
|
1158
|
-
background-color: var(--color-neutral-10);
|
|
1159
|
-
border: 1px solid var(--color-neutral-20);
|
|
1160
|
-
border-radius: var(--space-1, 0.25rem);
|
|
1161
|
-
padding: 0.125rem 0.375rem;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/* ---- Composition ---- */
|
|
1165
|
-
|
|
1166
|
-
/* Vertical stack with consistent gap — replaces manual margin chains */
|
|
1167
|
-
.stack {
|
|
1168
|
-
display: flex;
|
|
1169
|
-
flex-direction: column;
|
|
1170
|
-
gap: var(--space-4, 1rem);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
/* Horizontal grouping with wrapping — for tags, button rows, icon lists */
|
|
1174
|
-
.cluster {
|
|
1175
|
-
display: flex;
|
|
1176
|
-
flex-wrap: wrap;
|
|
1177
|
-
gap: var(--space-4, 1rem);
|
|
1178
|
-
align-items: center;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
/* ---- Layout ---- */
|
|
1182
|
-
|
|
1183
|
-
/* Constrained width container — 1100px max, full-width on small screens */
|
|
1184
|
-
.width-container {
|
|
1185
|
-
width: 100%;
|
|
1186
|
-
max-width: 1100px;
|
|
1187
|
-
margin-inline: auto;
|
|
1188
|
-
padding-inline: var(--space-4, 1rem);
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
@media (min-width: 640px) {
|
|
1192
|
-
.width-container {
|
|
1193
|
-
padding-inline: var(--space-6, 1.5rem);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
@media (min-width: 1024px) {
|
|
1198
|
-
.width-container {
|
|
1199
|
-
padding-inline: var(--space-8, 2rem);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
@media (min-width: 1140px) {
|
|
1204
|
-
.width-container {
|
|
1205
|
-
padding-inline: 0;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/* ---- Forms ---- */
|
|
1210
|
-
|
|
1211
|
-
.field-container {
|
|
1212
|
-
display: flex;
|
|
1213
|
-
flex-direction: column;
|
|
1214
|
-
gap: var(--space-2, 0.5rem);
|
|
1215
|
-
margin-bottom: var(--space-6, 1.5rem);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
.field-container label {
|
|
1219
|
-
display: block;
|
|
1220
|
-
font-weight: var(--font-weight-semibold, 600);
|
|
1221
|
-
color: var(--color-neutral-90);
|
|
1222
|
-
font-size: var(--text-base, 16px);
|
|
1223
|
-
line-height: 1.4;
|
|
1224
|
-
margin-bottom: var(--space-1, 0.25rem);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
fieldset {
|
|
1228
|
-
border: none;
|
|
1229
|
-
padding: 0;
|
|
1230
|
-
margin: 0 0 var(--space-6, 1.5rem);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
fieldset legend {
|
|
1234
|
-
display: block;
|
|
1235
|
-
font-size: var(--text-lg, 18px);
|
|
1236
|
-
font-weight: var(--font-weight-semibold, 600);
|
|
1237
|
-
margin-bottom: var(--space-3, 0.75rem);
|
|
1238
|
-
color: var(--color-neutral-90);
|
|
1239
|
-
padding: 0;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
.form-hint {
|
|
1243
|
-
font-size: var(--text-sm, 14px);
|
|
1244
|
-
color: var(--color-neutral-60);
|
|
1245
|
-
margin-bottom: var(--space-1, 0.25rem);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
input[type="text"],
|
|
1249
|
-
input[type="email"],
|
|
1250
|
-
input[type="password"],
|
|
1251
|
-
input[type="number"],
|
|
1252
|
-
input[type="tel"],
|
|
1253
|
-
input[type="url"],
|
|
1254
|
-
input[type="search"],
|
|
1255
|
-
input[type="date"],
|
|
1256
|
-
select,
|
|
1257
|
-
textarea {
|
|
1258
|
-
width: 100%;
|
|
1259
|
-
max-width: 100%;
|
|
1260
|
-
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
|
|
1261
|
-
border: 2px solid var(--color-neutral-30);
|
|
1262
|
-
border-radius: 8px;
|
|
1263
|
-
background-color: #ffffff;
|
|
1264
|
-
color: var(--color-neutral-90);
|
|
1265
|
-
font-family: inherit;
|
|
1266
|
-
font-size: var(--text-base, 16px);
|
|
1267
|
-
line-height: var(--leading-base, 1.6);
|
|
1268
|
-
appearance: none;
|
|
1269
|
-
transition: border-color 200ms ease, box-shadow 200ms ease;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
select {
|
|
1273
|
-
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
1274
|
-
background-position: right var(--space-2, 0.5rem) center;
|
|
1275
|
-
background-repeat: no-repeat;
|
|
1276
|
-
background-size: 1.5em 1.5em;
|
|
1277
|
-
padding-right: var(--space-10, 2.5rem);
|
|
1278
|
-
cursor: pointer;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
textarea {
|
|
1282
|
-
min-height: 120px;
|
|
1283
|
-
resize: vertical;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
input[type="text"]:focus,
|
|
1287
|
-
input[type="email"]:focus,
|
|
1288
|
-
input[type="password"]:focus,
|
|
1289
|
-
input[type="number"]:focus,
|
|
1290
|
-
input[type="tel"]:focus,
|
|
1291
|
-
input[type="url"]:focus,
|
|
1292
|
-
input[type="search"]:focus,
|
|
1293
|
-
input[type="date"]:focus,
|
|
1294
|
-
select:focus,
|
|
1295
|
-
textarea:focus {
|
|
1296
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1297
|
-
outline-offset: 3px;
|
|
1298
|
-
border-color: var(--color-neutral-80);
|
|
1299
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
.checkbox-group,
|
|
1303
|
-
.radio-group {
|
|
1304
|
-
display: flex;
|
|
1305
|
-
align-items: center;
|
|
1306
|
-
gap: var(--space-3, 0.75rem);
|
|
1307
|
-
margin-bottom: var(--space-4, 1rem);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
.checkbox-group label,
|
|
1311
|
-
.radio-group label {
|
|
1312
|
-
font-weight: var(--font-weight-normal, 400);
|
|
1313
|
-
margin-bottom: 0;
|
|
1314
|
-
cursor: pointer;
|
|
1315
|
-
font-size: var(--text-base, 16px);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
input[type="checkbox"] {
|
|
1319
|
-
width: 1.5rem;
|
|
1320
|
-
height: 1.5rem;
|
|
1321
|
-
margin: 0;
|
|
1322
|
-
cursor: pointer;
|
|
1323
|
-
accent-color: var(--color-brand-80);
|
|
1324
|
-
flex-shrink: 0;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
input[type="checkbox"]:focus {
|
|
1328
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1329
|
-
outline-offset: 3px;
|
|
1330
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
input[type="radio"] {
|
|
1334
|
-
width: 1.5rem;
|
|
1335
|
-
height: 1.5rem;
|
|
1336
|
-
margin: 0;
|
|
1337
|
-
border-radius: 50%;
|
|
1338
|
-
appearance: none;
|
|
1339
|
-
background-color: #ffffff;
|
|
1340
|
-
border: 2px solid var(--color-neutral-30);
|
|
1341
|
-
display: grid;
|
|
1342
|
-
place-content: center;
|
|
1343
|
-
cursor: pointer;
|
|
1344
|
-
flex-shrink: 0;
|
|
1345
|
-
transition: background-color 200ms ease, border-color 200ms ease;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
input[type="radio"]::before {
|
|
1349
|
-
content: "";
|
|
1350
|
-
width: 0.75rem;
|
|
1351
|
-
height: 0.75rem;
|
|
1352
|
-
border-radius: 50%;
|
|
1353
|
-
transform: scale(0);
|
|
1354
|
-
transition: 120ms transform ease-in-out;
|
|
1355
|
-
background-color: var(--color-brand-80);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
input[type="radio"]:checked {
|
|
1359
|
-
border-color: var(--color-brand-80);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
input[type="radio"]:checked::before {
|
|
1363
|
-
transform: scale(1);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
input[type="radio"]:hover {
|
|
1367
|
-
background-color: var(--color-brand-10);
|
|
1368
|
-
border-color: var(--color-brand-80);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
input[type="radio"]:focus {
|
|
1372
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1373
|
-
outline-offset: 3px;
|
|
1374
|
-
border-radius: 50%;
|
|
1375
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
input[aria-invalid="true"] {
|
|
1379
|
-
border-color: var(--color-error-80) !important;
|
|
1380
|
-
border-width: 3px;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
.form-error-message {
|
|
1384
|
-
font-size: var(--text-sm, 14px);
|
|
1385
|
-
font-weight: var(--font-weight-bold, 700);
|
|
1386
|
-
color: var(--color-error-80);
|
|
1387
|
-
margin-top: var(--space-1, 0.25rem);
|
|
1388
|
-
display: block;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
.error-summary {
|
|
1392
|
-
border: 4px solid var(--color-error-80);
|
|
1393
|
-
padding: var(--space-6, 1.5rem);
|
|
1394
|
-
margin-bottom: var(--space-8, 2rem);
|
|
1395
|
-
border-radius: 8px;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
.error-summary ul {
|
|
1399
|
-
list-style: disc;
|
|
1400
|
-
padding-left: var(--space-5, 1.25rem);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
.error-summary a {
|
|
1404
|
-
color: var(--color-error-80);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
/* ---- Buttons ---- */
|
|
1408
|
-
|
|
1409
|
-
.btn {
|
|
1410
|
-
display: inline-flex;
|
|
1411
|
-
align-items: center;
|
|
1412
|
-
justify-content: center;
|
|
1413
|
-
padding: var(--space-3, 0.75rem) var(--space-6, 1.5rem);
|
|
1414
|
-
font-weight: var(--font-weight-semibold, 600);
|
|
1415
|
-
border-radius: 8px;
|
|
1416
|
-
cursor: pointer;
|
|
1417
|
-
transition: background-color 200ms ease, border-color 200ms ease, color 200ms ease;
|
|
1418
|
-
border: 2px solid transparent;
|
|
1419
|
-
text-align: center;
|
|
1420
|
-
min-height: 3rem;
|
|
1421
|
-
font-size: var(--text-base, 16px);
|
|
1422
|
-
text-decoration: none;
|
|
1423
|
-
font-family: inherit;
|
|
1424
|
-
line-height: 1;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
.btn-primary {
|
|
1428
|
-
background-color: var(--color-brand-80);
|
|
1429
|
-
color: #ffffff;
|
|
1430
|
-
border-color: transparent;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
.btn-primary:hover {
|
|
1434
|
-
background-color: var(--color-brand-90);
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
.btn-primary:focus-visible {
|
|
1438
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1439
|
-
outline-offset: 3px;
|
|
1440
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
.btn-secondary {
|
|
1444
|
-
background-color: #ffffff;
|
|
1445
|
-
color: var(--color-accent-80);
|
|
1446
|
-
border-color: var(--color-accent-80);
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
.btn-secondary:hover {
|
|
1450
|
-
background-color: var(--color-accent-10);
|
|
1451
|
-
color: var(--color-accent-90);
|
|
1452
|
-
border-color: var(--color-accent-90);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
.btn-secondary:focus-visible {
|
|
1456
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1457
|
-
outline-offset: 3px;
|
|
1458
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
.btn-ghost {
|
|
1462
|
-
background-color: transparent;
|
|
1463
|
-
color: var(--color-neutral-80);
|
|
1464
|
-
border-color: transparent;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
.btn-ghost:hover {
|
|
1468
|
-
background-color: var(--color-neutral-10);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
.btn-ghost:focus-visible {
|
|
1472
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1473
|
-
outline-offset: 3px;
|
|
1474
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
.btn-danger {
|
|
1478
|
-
background-color: var(--color-error-80);
|
|
1479
|
-
color: #ffffff;
|
|
1480
|
-
border-color: transparent;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
.btn-danger:hover {
|
|
1484
|
-
background-color: var(--color-error-90);
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
.btn-danger:focus-visible {
|
|
1488
|
-
outline: 2px solid var(--color-neutral-80);
|
|
1489
|
-
outline-offset: 3px;
|
|
1490
|
-
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
.btn-sm {
|
|
1494
|
-
padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
|
|
1495
|
-
font-size: var(--text-sm, 14px);
|
|
1496
|
-
min-height: 2.25rem;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
.btn-lg {
|
|
1500
|
-
padding: var(--space-4, 1rem) var(--space-8, 2rem);
|
|
1501
|
-
font-size: var(--text-lg, 18px);
|
|
1502
|
-
min-height: 3.5rem;
|
|
1503
|
-
}
|
|
1504
|
-
`;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// ============================================================================
|
|
1508
|
-
// BUILD FUNCTION
|
|
1509
|
-
// ============================================================================
|
|
1510
|
-
|
|
1511
|
-
function buildFullFramework() {
|
|
1512
|
-
const config = getConfig();
|
|
1513
|
-
|
|
1514
|
-
console.log('Building EmilyCSS full framework...');
|
|
1515
|
-
|
|
1516
|
-
const colours = generateAllColours(config.colours);
|
|
1517
|
-
console.log(`✓ Generated ${Object.keys(colours).length} colour scales`);
|
|
1518
|
-
if (config.semanticColours) {
|
|
1519
|
-
console.log(`✓ Generated ${Object.keys(config.semanticColours).length} semantic colour tokens`);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
const spacing = generateSpacing(config.baseUnit, config.spacing.scale);
|
|
1523
|
-
console.log(`✓ Generated ${Object.keys(spacing).length} spacing values`);
|
|
1524
|
-
|
|
1525
|
-
const variablesCss = generateCSSVariables(colours, spacing, config);
|
|
1526
|
-
|
|
1527
|
-
let utilityCss = '';
|
|
1528
|
-
utilityCss += displayUtilities();
|
|
1529
|
-
utilityCss += generateSpacingUtilities(spacing);
|
|
1530
|
-
utilityCss += generateFlexboxUtilities(spacing);
|
|
1531
|
-
utilityCss += generateGridUtilities(spacing);
|
|
1532
|
-
utilityCss += sizingUtilities(spacing);
|
|
1533
|
-
utilityCss += generateTypographyUtilities(config);
|
|
1534
|
-
utilityCss += generateBorderUtilities(config);
|
|
1535
|
-
utilityCss += generateColourUtilities(colours);
|
|
1536
|
-
utilityCss += generateSemanticColourUtilities(config.semanticColours);
|
|
1537
|
-
utilityCss += positioningUtilities(spacing);
|
|
1538
|
-
utilityCss += overflowUtilities();
|
|
1539
|
-
utilityCss += transformUtilities(spacing);
|
|
1540
|
-
utilityCss += shadowUtilities();
|
|
1541
|
-
utilityCss += ringUtilities(colours);
|
|
1542
|
-
utilityCss += objectUtilities();
|
|
1543
|
-
utilityCss += tableListUtilities();
|
|
1544
|
-
utilityCss += svgUtilities(colours);
|
|
1545
|
-
utilityCss += formUtilities();
|
|
1546
|
-
utilityCss += verticalAlignUtilities();
|
|
1547
|
-
utilityCss += contentScrollUtilities();
|
|
1548
|
-
utilityCss += opacityUtilities();
|
|
1549
|
-
utilityCss += transitionUtilities();
|
|
1550
|
-
utilityCss += blendUtilities();
|
|
1551
|
-
utilityCss += cursorUtilities();
|
|
1552
|
-
utilityCss += accessibilityUtilities();
|
|
1553
|
-
utilityCss += containerUtilities();
|
|
1554
|
-
utilityCss += codeUtilities();
|
|
1555
|
-
utilityCss += animationUtilities();
|
|
1556
|
-
utilityCss += backdropUtilities();
|
|
1557
|
-
utilityCss += spaceUtilities(spacing);
|
|
1558
|
-
utilityCss += divideUtilities(spacing, colours);
|
|
1559
|
-
utilityCss += backgroundUtilities();
|
|
1560
|
-
utilityCss += filterUtilities();
|
|
1561
|
-
|
|
1562
|
-
utilityCss = addStateVariants(utilityCss);
|
|
1563
|
-
utilityCss = addAriaDataVariants(utilityCss);
|
|
1564
|
-
utilityCss = addDarkModeVariants(utilityCss);
|
|
1565
|
-
utilityCss = addResponsiveVariants(utilityCss, config);
|
|
1566
|
-
|
|
1567
|
-
const { fontFace, bodyFont } = generateFontCSS(config);
|
|
1568
|
-
|
|
1569
|
-
const fontLabel = typeof config.fontFamily === 'object'
|
|
1570
|
-
? 'heading: ' + (config.fontFamily.heading || 'system') + ', body: ' + (config.fontFamily.body || 'system')
|
|
1571
|
-
: (config.fontFamily || 'system');
|
|
1572
|
-
|
|
1573
|
-
console.log('✓ Font: ' + fontLabel);
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
margin-bottom:
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
display:
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
css += `@layer base
|
|
1712
|
-
css += `@layer
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
.replace(
|
|
1757
|
-
.
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
1783
|
-
profile.
|
|
1784
|
-
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
console.log('
|
|
1850
|
-
console.log('
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { generateManifest } = require('./manifest');
|
|
6
|
+
const { generateIntellisense } = require('./intellisense');
|
|
7
|
+
const {
|
|
8
|
+
getConfigPath,
|
|
9
|
+
getConfig,
|
|
10
|
+
getFullCssPath,
|
|
11
|
+
getProductionCssPath,
|
|
12
|
+
getManifestSettings,
|
|
13
|
+
getManifestOutputPath,
|
|
14
|
+
getIntellisenseSettings,
|
|
15
|
+
getIntellisenseOutputPath,
|
|
16
|
+
ensureDirectoryForFile,
|
|
17
|
+
getSourceDir,
|
|
18
|
+
} = require('./config');
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// COLOUR GENERATION
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Generate 10-shade colour scale using OKLCH (perceptually uniform colour space)
|
|
25
|
+
// OKLCH produces visually even steps across all hues — unlike HSL which creates
|
|
26
|
+
// muddy mid-tones on warm colours (yellows, greens, oranges).
|
|
27
|
+
//
|
|
28
|
+
// Conversion pipeline: Hex → sRGB → Linear RGB → OKLab → OKLCH → (modify L) → reverse
|
|
29
|
+
// No external dependencies. Maths from Björn Ottosson's OKLab specification.
|
|
30
|
+
//
|
|
31
|
+
// Input: #0077b6 → Output: { 10: '#...', 20: '#...', ..., 100: '#...' }
|
|
32
|
+
|
|
33
|
+
// sRGB component to linear light
|
|
34
|
+
function srgbToLinear(c) {
|
|
35
|
+
const val = c / 255;
|
|
36
|
+
return val <= 0.04045 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Linear light component to sRGB (clamped to 0–255)
|
|
40
|
+
function linearToSrgb(c) {
|
|
41
|
+
const clamped = Math.max(0, Math.min(1, c));
|
|
42
|
+
const out = clamped <= 0.0031308
|
|
43
|
+
? 12.92 * clamped
|
|
44
|
+
: 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
45
|
+
return Math.round(Math.max(0, Math.min(1, out)) * 255);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Hex string → OKLCH { l, c, h }
|
|
49
|
+
function hexToOklch(hex) {
|
|
50
|
+
const r = srgbToLinear(parseInt(hex.slice(1, 3), 16));
|
|
51
|
+
const g = srgbToLinear(parseInt(hex.slice(3, 5), 16));
|
|
52
|
+
const b = srgbToLinear(parseInt(hex.slice(5, 7), 16));
|
|
53
|
+
|
|
54
|
+
// Linear RGB → OKLab (M1 matrix then cube-root then M2 matrix)
|
|
55
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
56
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
57
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
58
|
+
|
|
59
|
+
const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
|
|
60
|
+
const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
|
|
61
|
+
const bv = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
|
|
62
|
+
|
|
63
|
+
// OKLab → OKLCH
|
|
64
|
+
const C = Math.sqrt(a * a + bv * bv);
|
|
65
|
+
const H = (Math.atan2(bv, a) * 180) / Math.PI;
|
|
66
|
+
|
|
67
|
+
return { l: L, c: C, h: H < 0 ? H + 360 : H };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// OKLCH { l, c, h } → hex string
|
|
71
|
+
function oklchToHex(l, c, h) {
|
|
72
|
+
// OKLCH → OKLab
|
|
73
|
+
const hRad = (h * Math.PI) / 180;
|
|
74
|
+
const a = c * Math.cos(hRad);
|
|
75
|
+
const bv = c * Math.sin(hRad);
|
|
76
|
+
|
|
77
|
+
// OKLab → Linear RGB (M2 inverse then cube then M1 inverse)
|
|
78
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * bv;
|
|
79
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * bv;
|
|
80
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * bv;
|
|
81
|
+
|
|
82
|
+
const lc = l_ * l_ * l_;
|
|
83
|
+
const mc = m_ * m_ * m_;
|
|
84
|
+
const sc = s_ * s_ * s_;
|
|
85
|
+
|
|
86
|
+
const r = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
|
|
87
|
+
const g = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
|
|
88
|
+
const b = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
89
|
+
|
|
90
|
+
const rOut = linearToSrgb(r).toString(16).padStart(2, '0');
|
|
91
|
+
const gOut = linearToSrgb(g).toString(16).padStart(2, '0');
|
|
92
|
+
const bOut = linearToSrgb(b).toString(16).padStart(2, '0');
|
|
93
|
+
|
|
94
|
+
return `#${rOut}${gOut}${bOut}`.toUpperCase();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function generateColourScale(baseHex) {
|
|
98
|
+
const { l: baseL, c: baseC, h: baseH } = hexToOklch(baseHex);
|
|
99
|
+
const scale = {};
|
|
100
|
+
|
|
101
|
+
// Shade scale: 10 = near-white, 80 = exact input colour, 100 = near-black
|
|
102
|
+
// Lightness targets in OKLCH (0–1 scale):
|
|
103
|
+
// shade 10 → L ≈ 0.97 (very light tint)
|
|
104
|
+
// shade 80 → L = baseL (exact input)
|
|
105
|
+
// shade 100 → L ≈ 0.15 (very dark tone)
|
|
106
|
+
//
|
|
107
|
+
// Chroma is preserved from the base colour throughout — hue is never shifted.
|
|
108
|
+
// At extreme lightness values chroma is gently reduced to stay in sRGB gamut.
|
|
109
|
+
|
|
110
|
+
const LIGHT_L = 0.97; // shade 10 lightness
|
|
111
|
+
const DARK_L = 0.15; // shade 100 lightness
|
|
112
|
+
|
|
113
|
+
const steps = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
114
|
+
|
|
115
|
+
steps.forEach(step => {
|
|
116
|
+
if (step === 80) {
|
|
117
|
+
scale[step] = baseHex.toUpperCase();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let newL;
|
|
122
|
+
if (step < 80) {
|
|
123
|
+
// 10–70: interpolate from LIGHT_L down to baseL
|
|
124
|
+
const t = (step / 80); // 0 → 1 as step goes 0 → 80
|
|
125
|
+
newL = LIGHT_L - t * (LIGHT_L - baseL);
|
|
126
|
+
} else {
|
|
127
|
+
// 90–100: interpolate from baseL down to DARK_L
|
|
128
|
+
const t = (step - 80) / 20; // 0 → 1 as step goes 80 → 100
|
|
129
|
+
newL = baseL - t * (baseL - DARK_L);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Reduce chroma slightly at extremes to avoid out-of-gamut clipping
|
|
133
|
+
const chromaScale = 1 - Math.max(0, (newL - 0.90) / 0.07) * 0.5 // reduce near white
|
|
134
|
+
- Math.max(0, (0.25 - newL) / 0.10) * 0.5; // reduce near black
|
|
135
|
+
const newC = baseC * Math.max(0, chromaScale);
|
|
136
|
+
|
|
137
|
+
scale[step] = oklchToHex(newL, newC, baseH);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return scale;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function generateAllColours(colourConfig) {
|
|
144
|
+
const allColours = {};
|
|
145
|
+
|
|
146
|
+
Object.entries(colourConfig).forEach(([name, baseHex]) => {
|
|
147
|
+
allColours[name] = generateColourScale(baseHex);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return allColours;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// SPACING SCALE
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
function generateSpacing(baseUnit, scale) {
|
|
158
|
+
// Spacing values are defined explicitly in emily.config.json under spacing.scale.
|
|
159
|
+
// The baseUnit key in config is informational only — it documents the design intent
|
|
160
|
+
// (e.g. "this system is based on 8px") but does not drive generation.
|
|
161
|
+
return scale;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// FONT PRESETS
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
// Font presets define the CSS font-family stack only.
|
|
169
|
+
// Loading the actual font files is the user's responsibility — link them in your HTML
|
|
170
|
+
// or use @fontsource packages in your build. emily-css does not generate @import rules
|
|
171
|
+
// for external CDNs so it stays self-contained and works offline.
|
|
172
|
+
// See docs: https://emilyui.com/docs/getting-started
|
|
173
|
+
const FONT_PRESETS = {
|
|
174
|
+
'system': {
|
|
175
|
+
stack: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
176
|
+
},
|
|
177
|
+
'inter': {
|
|
178
|
+
name: 'Inter',
|
|
179
|
+
stack: '"Inter", system-ui, sans-serif',
|
|
180
|
+
},
|
|
181
|
+
'lexend': {
|
|
182
|
+
name: 'Lexend',
|
|
183
|
+
stack: '"Lexend", system-ui, sans-serif',
|
|
184
|
+
},
|
|
185
|
+
'georgia': {
|
|
186
|
+
stack: 'Georgia, "Times New Roman", serif',
|
|
187
|
+
},
|
|
188
|
+
'dm-sans': {
|
|
189
|
+
name: 'DM Sans',
|
|
190
|
+
stack: '"DM Sans", system-ui, sans-serif',
|
|
191
|
+
},
|
|
192
|
+
'nunito': {
|
|
193
|
+
name: 'Nunito',
|
|
194
|
+
stack: '"Nunito", system-ui, sans-serif',
|
|
195
|
+
},
|
|
196
|
+
'atkinson': {
|
|
197
|
+
name: 'Atkinson Hyperlegible',
|
|
198
|
+
stack: '"Atkinson Hyperlegible", system-ui, sans-serif',
|
|
199
|
+
},
|
|
200
|
+
'mono': {
|
|
201
|
+
stack: '"Menlo", "Monaco", "Courier New", monospace',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function generateFontCSS(config) {
|
|
206
|
+
// Support both legacy string format and new { heading, body } object format
|
|
207
|
+
const fontConfig = config.fontFamily || 'system';
|
|
208
|
+
let headingKey, bodyKey;
|
|
209
|
+
|
|
210
|
+
if (typeof fontConfig === 'object') {
|
|
211
|
+
headingKey = (fontConfig.heading || 'system').toLowerCase();
|
|
212
|
+
bodyKey = (fontConfig.body || 'system').toLowerCase();
|
|
213
|
+
} else {
|
|
214
|
+
headingKey = fontConfig.toLowerCase();
|
|
215
|
+
bodyKey = fontConfig.toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const headingPreset = FONT_PRESETS[headingKey] || FONT_PRESETS['system'];
|
|
219
|
+
const bodyPreset = FONT_PRESETS[bodyKey] || FONT_PRESETS['system'];
|
|
220
|
+
|
|
221
|
+
let fontFace = '';
|
|
222
|
+
let bodyFont = '';
|
|
223
|
+
|
|
224
|
+
bodyFont += ` body {\n font-family: ${bodyPreset.stack};\n font-synthesis: style;\n }\n`;
|
|
225
|
+
bodyFont += ` h1, h2, h3, h4, h5, h6 {\n font-family: ${headingPreset.stack};\n }\n`;
|
|
226
|
+
|
|
227
|
+
return { fontFace, bodyFont };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// CSS VARIABLE GENERATION
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
function generateCSSVariables(colours, spacing, config) {
|
|
235
|
+
let css = `:root {\n`;
|
|
236
|
+
|
|
237
|
+
// Colour variables (full shade scale)
|
|
238
|
+
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
239
|
+
Object.entries(shades).forEach(([shade, hex]) => {
|
|
240
|
+
css += ` --color-${colourName}-${shade}: ${hex};\n`;
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Semantic colour variables (single value, no shade scale)
|
|
245
|
+
if (config.semanticColours) {
|
|
246
|
+
Object.entries(config.semanticColours).forEach(([name, hex]) => {
|
|
247
|
+
css += ` --color-${name}: ${hex};\n`;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
css += ` --focus-ring-glow: color-mix(in srgb, var(--color-brand-80) 12%, transparent);\n`;
|
|
251
|
+
|
|
252
|
+
// Spacing variables
|
|
253
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
254
|
+
css += ` --space-${key}: ${value};\n`;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Font size variables with line-height
|
|
258
|
+
config.typography.fontSizes.forEach(fontSize => {
|
|
259
|
+
const sizeVal = parseInt(fontSize.value);
|
|
260
|
+
css += ` --text-${fontSize.name}: ${fontSize.value};\n`;
|
|
261
|
+
css += ` --leading-${fontSize.name}: ${fontSize.lineHeight};\n`;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Font weight variables
|
|
265
|
+
Object.entries(config.typography.fontWeights).forEach(([name, weight]) => {
|
|
266
|
+
css += ` --font-weight-${name}: ${weight};\n`;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Breakpoints
|
|
270
|
+
Object.entries(config.breakpoints).forEach(([name, value]) => {
|
|
271
|
+
css += ` --breakpoint-${name}: ${value};\n`;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Shadows
|
|
275
|
+
Object.entries(config.shadows).forEach(([name, shadow]) => {
|
|
276
|
+
css += ` --shadow-${name}: ${shadow};\n`;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Z-index
|
|
280
|
+
Object.entries(config.zIndex).forEach(([name, value]) => {
|
|
281
|
+
css += ` --z-${name}: ${value};\n`;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Transitions
|
|
285
|
+
css += ` --transition-duration: ${config.transitions.base};\n`;
|
|
286
|
+
css += ` --transition-timing: ${config.transitions.timing};\n`;
|
|
287
|
+
|
|
288
|
+
css += `}\n\n`;
|
|
289
|
+
return css;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const {
|
|
293
|
+
displayUtilities,
|
|
294
|
+
sizingUtilities,
|
|
295
|
+
positioningUtilities,
|
|
296
|
+
overflowUtilities,
|
|
297
|
+
opacityUtilities,
|
|
298
|
+
transitionUtilities,
|
|
299
|
+
transformUtilities,
|
|
300
|
+
shadowUtilities,
|
|
301
|
+
ringUtilities,
|
|
302
|
+
objectUtilities,
|
|
303
|
+
tableListUtilities,
|
|
304
|
+
svgUtilities,
|
|
305
|
+
formUtilities,
|
|
306
|
+
verticalAlignUtilities,
|
|
307
|
+
contentScrollUtilities,
|
|
308
|
+
blendUtilities,
|
|
309
|
+
cursorUtilities,
|
|
310
|
+
accessibilityUtilities,
|
|
311
|
+
containerUtilities,
|
|
312
|
+
codeUtilities,
|
|
313
|
+
animationUtilities,
|
|
314
|
+
backdropUtilities,
|
|
315
|
+
spaceUtilities,
|
|
316
|
+
divideUtilities,
|
|
317
|
+
backgroundUtilities,
|
|
318
|
+
filterUtilities,
|
|
319
|
+
} = require('./generators');
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// SPACING UTILITIES
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
function escapeClassName(key) {
|
|
326
|
+
// Escape dots in class names for CSS (e.g., "0.5" becomes "0\.5")
|
|
327
|
+
return key.replace(/\./g, '\\.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function generateSpacingUtilities(spacing) {
|
|
331
|
+
let css = `/* Spacing: Padding & Margin */\n`;
|
|
332
|
+
|
|
333
|
+
// Padding
|
|
334
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
335
|
+
const escaped = escapeClassName(key);
|
|
336
|
+
css += `.p-${escaped} { padding: ${value}; }\n`;
|
|
337
|
+
css += `.px-${escaped} { padding-left: ${value}; padding-right: ${value}; }\n`;
|
|
338
|
+
css += `.py-${escaped} { padding-top: ${value}; padding-bottom: ${value}; }\n`;
|
|
339
|
+
css += `.pt-${escaped} { padding-top: ${value}; }\n`;
|
|
340
|
+
css += `.pr-${escaped} { padding-right: ${value}; }\n`;
|
|
341
|
+
css += `.pb-${escaped} { padding-bottom: ${value}; }\n`;
|
|
342
|
+
css += `.pl-${escaped} { padding-left: ${value}; }\n`;
|
|
343
|
+
css += `.ps-${escaped} { padding-inline-start: ${value}; }\n`;
|
|
344
|
+
css += `.pe-${escaped} { padding-inline-end: ${value}; }\n`;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Margin
|
|
348
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
349
|
+
const escaped = escapeClassName(key);
|
|
350
|
+
css += `.m-${escaped} { margin: ${value}; }\n`;
|
|
351
|
+
css += `.mx-${escaped} { margin-left: ${value}; margin-right: ${value}; }\n`;
|
|
352
|
+
css += `.my-${escaped} { margin-top: ${value}; margin-bottom: ${value}; }\n`;
|
|
353
|
+
css += `.mt-${escaped} { margin-top: ${value}; }\n`;
|
|
354
|
+
css += `.mr-${escaped} { margin-right: ${value}; }\n`;
|
|
355
|
+
css += `.mb-${escaped} { margin-bottom: ${value}; }\n`;
|
|
356
|
+
css += `.ml-${escaped} { margin-left: ${value}; }\n`;
|
|
357
|
+
css += `.ms-${escaped} { margin-inline-start: ${value}; }\n`;
|
|
358
|
+
css += `.me-${escaped} { margin-inline-end: ${value}; }\n`;
|
|
359
|
+
if (value !== '0' && value !== '0px') {
|
|
360
|
+
css += `.-m-${escaped} { margin: -${value}; }\n`;
|
|
361
|
+
css += `.-mx-${escaped} { margin-left: -${value}; margin-right: -${value}; }\n`;
|
|
362
|
+
css += `.-my-${escaped} { margin-top: -${value}; margin-bottom: -${value}; }\n`;
|
|
363
|
+
css += `.-mt-${escaped} { margin-top: -${value}; }\n`;
|
|
364
|
+
css += `.-mr-${escaped} { margin-right: -${value}; }\n`;
|
|
365
|
+
css += `.-mb-${escaped} { margin-bottom: -${value}; }\n`;
|
|
366
|
+
css += `.-ml-${escaped} { margin-left: -${value}; }\n`;
|
|
367
|
+
css += `.-ms-${escaped} { margin-inline-start: -${value}; }\n`;
|
|
368
|
+
css += `.-me-${escaped} { margin-inline-end: -${value}; }\n`;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Margin auto
|
|
373
|
+
css += `.mx-auto { margin-left: auto; margin-right: auto; }\n`;
|
|
374
|
+
css += `.my-auto { margin-top: auto; margin-bottom: auto; }\n`;
|
|
375
|
+
|
|
376
|
+
css += `\n`;
|
|
377
|
+
return css;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// FLEXBOX UTILITIES
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
function generateFlexboxUtilities(spacing) {
|
|
385
|
+
let css = `/* Flexbox */\n`;
|
|
386
|
+
|
|
387
|
+
css += `.inline-flex { display: inline-flex; }\n`;
|
|
388
|
+
|
|
389
|
+
// Direction
|
|
390
|
+
css += `.flex-row { flex-direction: row; }\n`;
|
|
391
|
+
css += `.flex-col { flex-direction: column; }\n`;
|
|
392
|
+
css += `.flex-row-reverse { flex-direction: row-reverse; }\n`;
|
|
393
|
+
css += `.flex-col-reverse { flex-direction: column-reverse; }\n`;
|
|
394
|
+
|
|
395
|
+
// Wrap
|
|
396
|
+
css += `.flex-wrap { flex-wrap: wrap; }\n`;
|
|
397
|
+
css += `.flex-nowrap { flex-wrap: nowrap; }\n`;
|
|
398
|
+
css += `.flex-wrap-reverse { flex-wrap: wrap-reverse; }\n`;
|
|
399
|
+
|
|
400
|
+
// Flex shorthand
|
|
401
|
+
css += `.flex-1 { flex: 1 1 0%; }\n`;
|
|
402
|
+
css += `.flex-auto { flex: 1 1 auto; }\n`;
|
|
403
|
+
css += `.flex-initial { flex: 0 1 auto; }\n`;
|
|
404
|
+
css += `.flex-none { flex: none; }\n`;
|
|
405
|
+
|
|
406
|
+
// Grow/shrink
|
|
407
|
+
css += `.grow { flex-grow: 1; }\n`;
|
|
408
|
+
css += `.grow-0 { flex-grow: 0; }\n`;
|
|
409
|
+
css += `.shrink { flex-shrink: 1; }\n`;
|
|
410
|
+
css += `.shrink-0 { flex-shrink: 0; }\n`;
|
|
411
|
+
|
|
412
|
+
// Flex basis
|
|
413
|
+
css += `.basis-auto { flex-basis: auto; }\n`;
|
|
414
|
+
css += `.basis-full { flex-basis: 100%; }\n`;
|
|
415
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
416
|
+
const escaped = escapeClassName(key);
|
|
417
|
+
css += `.basis-${escaped} { flex-basis: ${value}; }\n`;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const fractions = {
|
|
421
|
+
'1\\/2': '50%', '1\\/3': '33.333333%', '2\\/3': '66.666667%',
|
|
422
|
+
'1\\/4': '25%', '2\\/4': '50%', '3\\/4': '75%',
|
|
423
|
+
'1\\/5': '20%', '2\\/5': '40%', '3\\/5': '60%', '4\\/5': '80%',
|
|
424
|
+
'1\\/6': '16.666667%', '2\\/6': '33.333333%', '3\\/6': '50%', '4\\/6': '66.666667%', '5\\/6': '83.333333%',
|
|
425
|
+
'1\\/12': '8.333333%', '2\\/12': '16.666667%', '3\\/12': '25%', '4\\/12': '33.333333%', '5\\/12': '41.666667%', '6\\/12': '50%', '7\\/12': '58.333333%', '8\\/12': '66.666667%', '9\\/12': '75%', '10\\/12': '83.333333%', '11\\/12': '91.666667%'
|
|
426
|
+
};
|
|
427
|
+
Object.entries(fractions).forEach(([name, value]) => {
|
|
428
|
+
css += `.basis-${name} { flex-basis: ${value}; }\n`;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Order
|
|
432
|
+
css += `.order-first { order: -9999; }\n`;
|
|
433
|
+
css += `.order-last { order: 9999; }\n`;
|
|
434
|
+
css += `.order-none { order: 0; }\n`;
|
|
435
|
+
for (let i = 1; i <= 12; i++) {
|
|
436
|
+
css += `.order-${i} { order: ${i}; }\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Justify (main axis)
|
|
440
|
+
css += `.justify-normal { justify-content: normal; }\n`;
|
|
441
|
+
css += `.justify-start { justify-content: flex-start; }\n`;
|
|
442
|
+
css += `.justify-end { justify-content: flex-end; }\n`;
|
|
443
|
+
css += `.justify-center { justify-content: center; }\n`;
|
|
444
|
+
css += `.justify-between { justify-content: space-between; }\n`;
|
|
445
|
+
css += `.justify-around { justify-content: space-around; }\n`;
|
|
446
|
+
css += `.justify-evenly { justify-content: space-evenly; }\n`;
|
|
447
|
+
css += `.justify-stretch { justify-content: stretch; }\n`;
|
|
448
|
+
|
|
449
|
+
// Content alignment
|
|
450
|
+
css += `.content-normal { align-content: normal; }\n`;
|
|
451
|
+
css += `.content-center { align-content: center; }\n`;
|
|
452
|
+
css += `.content-start { align-content: flex-start; }\n`;
|
|
453
|
+
css += `.content-end { align-content: flex-end; }\n`;
|
|
454
|
+
css += `.content-between { align-content: space-between; }\n`;
|
|
455
|
+
css += `.content-around { align-content: space-around; }\n`;
|
|
456
|
+
css += `.content-evenly { align-content: space-evenly; }\n`;
|
|
457
|
+
css += `.content-baseline { align-content: baseline; }\n`;
|
|
458
|
+
css += `.content-stretch { align-content: stretch; }\n`;
|
|
459
|
+
|
|
460
|
+
// Items (cross axis)
|
|
461
|
+
css += `.items-start { align-items: flex-start; }\n`;
|
|
462
|
+
css += `.items-end { align-items: flex-end; }\n`;
|
|
463
|
+
css += `.items-center { align-items: center; }\n`;
|
|
464
|
+
css += `.items-baseline { align-items: baseline; }\n`;
|
|
465
|
+
css += `.items-stretch { align-items: stretch; }\n`;
|
|
466
|
+
|
|
467
|
+
// Self alignment
|
|
468
|
+
css += `.self-auto { align-self: auto; }\n`;
|
|
469
|
+
css += `.self-start { align-self: flex-start; }\n`;
|
|
470
|
+
css += `.self-end { align-self: flex-end; }\n`;
|
|
471
|
+
css += `.self-center { align-self: center; }\n`;
|
|
472
|
+
css += `.self-stretch { align-self: stretch; }\n`;
|
|
473
|
+
css += `.self-baseline { align-self: baseline; }\n`;
|
|
474
|
+
|
|
475
|
+
// Place utilities
|
|
476
|
+
css += `.place-content-center { place-content: center; }\n`;
|
|
477
|
+
css += `.place-content-start { place-content: start; }\n`;
|
|
478
|
+
css += `.place-content-end { place-content: end; }\n`;
|
|
479
|
+
css += `.place-content-between { place-content: space-between; }\n`;
|
|
480
|
+
css += `.place-content-around { place-content: space-around; }\n`;
|
|
481
|
+
css += `.place-content-evenly { place-content: space-evenly; }\n`;
|
|
482
|
+
css += `.place-content-baseline { place-content: baseline; }\n`;
|
|
483
|
+
css += `.place-content-stretch { place-content: stretch; }\n`;
|
|
484
|
+
css += `.place-items-start { place-items: start; }\n`;
|
|
485
|
+
css += `.place-items-end { place-items: end; }\n`;
|
|
486
|
+
css += `.place-items-center { place-items: center; }\n`;
|
|
487
|
+
css += `.place-items-baseline { place-items: baseline; }\n`;
|
|
488
|
+
css += `.place-items-stretch { place-items: stretch; }\n`;
|
|
489
|
+
css += `.place-self-auto { place-self: auto; }\n`;
|
|
490
|
+
css += `.place-self-start { place-self: start; }\n`;
|
|
491
|
+
css += `.place-self-end { place-self: end; }\n`;
|
|
492
|
+
css += `.place-self-center { place-self: center; }\n`;
|
|
493
|
+
css += `.place-self-stretch { place-self: stretch; }\n`;
|
|
494
|
+
|
|
495
|
+
css += `\n`;
|
|
496
|
+
return css;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ============================================================================
|
|
500
|
+
// GRID UTILITIES
|
|
501
|
+
// ============================================================================
|
|
502
|
+
|
|
503
|
+
function generateGridUtilities(spacing) {
|
|
504
|
+
let css = `/* Grid */\n`;
|
|
505
|
+
|
|
506
|
+
css += `.inline-grid { display: inline-grid; }\n`;
|
|
507
|
+
|
|
508
|
+
css += `.grid-cols-none { grid-template-columns: none; }\n`;
|
|
509
|
+
css += `.grid-cols-subgrid { grid-template-columns: subgrid; }\n`;
|
|
510
|
+
for (let i = 1; i <= 12; i++) {
|
|
511
|
+
css += `.grid-cols-${i} { grid-template-columns: repeat(${i}, minmax(0, 1fr)); }\n`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
css += `.grid-rows-none { grid-template-rows: none; }\n`;
|
|
515
|
+
css += `.grid-rows-subgrid { grid-template-rows: subgrid; }\n`;
|
|
516
|
+
for (let i = 1; i <= 12; i++) {
|
|
517
|
+
css += `.grid-rows-${i} { grid-template-rows: repeat(${i}, minmax(0, 1fr)); }\n`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
for (let i = 1; i <= 12; i++) {
|
|
521
|
+
css += `.col-span-${i} { grid-column: span ${i} / span ${i}; }\n`;
|
|
522
|
+
}
|
|
523
|
+
css += `.col-span-full { grid-column: 1 / -1; }\n`;
|
|
524
|
+
css += `.col-auto { grid-column: auto; }\n`;
|
|
525
|
+
for (let i = 1; i <= 13; i++) {
|
|
526
|
+
css += `.col-start-${i} { grid-column-start: ${i}; }\n`;
|
|
527
|
+
css += `.col-end-${i} { grid-column-end: ${i}; }\n`;
|
|
528
|
+
}
|
|
529
|
+
css += `.col-start-auto { grid-column-start: auto; }\n`;
|
|
530
|
+
css += `.col-end-auto { grid-column-end: auto; }\n`;
|
|
531
|
+
|
|
532
|
+
for (let i = 1; i <= 12; i++) {
|
|
533
|
+
css += `.row-span-${i} { grid-row: span ${i} / span ${i}; }\n`;
|
|
534
|
+
}
|
|
535
|
+
css += `.row-span-full { grid-row: 1 / -1; }\n`;
|
|
536
|
+
css += `.row-auto { grid-row: auto; }\n`;
|
|
537
|
+
for (let i = 1; i <= 13; i++) {
|
|
538
|
+
css += `.row-start-${i} { grid-row-start: ${i}; }\n`;
|
|
539
|
+
css += `.row-end-${i} { grid-row-end: ${i}; }\n`;
|
|
540
|
+
}
|
|
541
|
+
css += `.row-start-auto { grid-row-start: auto; }\n`;
|
|
542
|
+
css += `.row-end-auto { grid-row-end: auto; }\n`;
|
|
543
|
+
|
|
544
|
+
css += `.grid-flow-row { grid-auto-flow: row; }\n`;
|
|
545
|
+
css += `.grid-flow-col { grid-auto-flow: column; }\n`;
|
|
546
|
+
css += `.grid-flow-dense { grid-auto-flow: dense; }\n`;
|
|
547
|
+
css += `.grid-flow-row-dense { grid-auto-flow: row dense; }\n`;
|
|
548
|
+
css += `.grid-flow-col-dense { grid-auto-flow: column dense; }\n`;
|
|
549
|
+
|
|
550
|
+
css += `.auto-cols-auto { grid-auto-columns: auto; }\n`;
|
|
551
|
+
css += `.auto-cols-min { grid-auto-columns: min-content; }\n`;
|
|
552
|
+
css += `.auto-cols-max { grid-auto-columns: max-content; }\n`;
|
|
553
|
+
css += `.auto-cols-fr { grid-auto-columns: minmax(0, 1fr); }\n`;
|
|
554
|
+
css += `.auto-rows-auto { grid-auto-rows: auto; }\n`;
|
|
555
|
+
css += `.auto-rows-min { grid-auto-rows: min-content; }\n`;
|
|
556
|
+
css += `.auto-rows-max { grid-auto-rows: max-content; }\n`;
|
|
557
|
+
css += `.auto-rows-fr { grid-auto-rows: minmax(0, 1fr); }\n`;
|
|
558
|
+
|
|
559
|
+
Object.entries(spacing).forEach(([key, value]) => {
|
|
560
|
+
const escaped = escapeClassName(key);
|
|
561
|
+
css += `.gap-${escaped} { gap: ${value}; }\n`;
|
|
562
|
+
css += `.gap-x-${escaped} { column-gap: ${value}; }\n`;
|
|
563
|
+
css += `.gap-y-${escaped} { row-gap: ${value}; }\n`;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
css += `.justify-items-start { justify-items: start; }\n`;
|
|
567
|
+
css += `.justify-items-end { justify-items: end; }\n`;
|
|
568
|
+
css += `.justify-items-center { justify-items: center; }\n`;
|
|
569
|
+
css += `.justify-items-stretch { justify-items: stretch; }\n`;
|
|
570
|
+
css += `.justify-self-auto { justify-self: auto; }\n`;
|
|
571
|
+
css += `.justify-self-start { justify-self: start; }\n`;
|
|
572
|
+
css += `.justify-self-end { justify-self: end; }\n`;
|
|
573
|
+
css += `.justify-self-center { justify-self: center; }\n`;
|
|
574
|
+
css += `.justify-self-stretch { justify-self: stretch; }\n`;
|
|
575
|
+
|
|
576
|
+
css += `\n`;
|
|
577
|
+
return css;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// TYPOGRAPHY UTILITIES
|
|
582
|
+
// ============================================================================
|
|
583
|
+
|
|
584
|
+
function generateTypographyUtilities(config) {
|
|
585
|
+
let css = `/* Typography */\n`;
|
|
586
|
+
|
|
587
|
+
config.typography.fontSizes.forEach(fontSize => {
|
|
588
|
+
css += `.text-${fontSize.name} { font-size: var(--text-${fontSize.name}); line-height: ${fontSize.lineHeight}; }\n`;
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const fontWeightDefaults = {
|
|
592
|
+
thin: 100,
|
|
593
|
+
extralight: 200,
|
|
594
|
+
light: 300,
|
|
595
|
+
normal: 400,
|
|
596
|
+
medium: 500,
|
|
597
|
+
semibold: 600,
|
|
598
|
+
bold: 700,
|
|
599
|
+
extrabold: 800,
|
|
600
|
+
black: 900,
|
|
601
|
+
};
|
|
602
|
+
const resolvedFontWeights = {
|
|
603
|
+
...fontWeightDefaults,
|
|
604
|
+
...(config.typography.fontWeights || {}),
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
Object.entries(resolvedFontWeights).forEach(([name, weight]) => {
|
|
608
|
+
css += `.font-${name} { font-weight: ${weight}; }\n`;
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
css += `.italic { font-style: italic; }\n`;
|
|
612
|
+
css += `.not-italic { font-style: normal; }\n`;
|
|
613
|
+
|
|
614
|
+
css += `.text-left { text-align: left; }\n`;
|
|
615
|
+
css += `.text-center { text-align: center; }\n`;
|
|
616
|
+
css += `.text-right { text-align: right; }\n`;
|
|
617
|
+
css += `.text-justify { text-align: justify; }\n`;
|
|
618
|
+
css += `.text-start { text-align: start; }\n`;
|
|
619
|
+
css += `.text-end { text-align: end; }\n`;
|
|
620
|
+
|
|
621
|
+
css += `.whitespace-normal { white-space: normal; }\n`;
|
|
622
|
+
css += `.whitespace-nowrap { white-space: nowrap; }\n`;
|
|
623
|
+
css += `.whitespace-pre { white-space: pre; }\n`;
|
|
624
|
+
css += `.whitespace-pre-line { white-space: pre-line; }\n`;
|
|
625
|
+
css += `.whitespace-pre-wrap { white-space: pre-wrap; }\n`;
|
|
626
|
+
css += `.whitespace-break-spaces { white-space: break-spaces; }\n`;
|
|
627
|
+
css += `.text-wrap { text-wrap: wrap; }\n`;
|
|
628
|
+
css += `.text-nowrap { text-wrap: nowrap; }\n`;
|
|
629
|
+
css += `.text-balance { text-wrap: balance; }\n`;
|
|
630
|
+
css += `.text-pretty { text-wrap: pretty; }\n`;
|
|
631
|
+
css += `.break-normal { overflow-wrap: normal; word-break: normal; }\n`;
|
|
632
|
+
css += `.break-words { overflow-wrap: break-word; }\n`;
|
|
633
|
+
css += `.break-all { word-break: break-all; }\n`;
|
|
634
|
+
css += `.break-keep { word-break: keep-all; }\n`;
|
|
635
|
+
css += `.hyphens-none { hyphens: none; }\n`;
|
|
636
|
+
css += `.hyphens-manual { hyphens: manual; }\n`;
|
|
637
|
+
css += `.hyphens-auto { hyphens: auto; }\n`;
|
|
638
|
+
|
|
639
|
+
css += `.leading-none { line-height: 1; }\n`;
|
|
640
|
+
css += `.leading-tight { line-height: 1.25; }\n`;
|
|
641
|
+
css += `.leading-snug { line-height: 1.375; }\n`;
|
|
642
|
+
css += `.leading-normal { line-height: 1.5; }\n`;
|
|
643
|
+
css += `.leading-relaxed { line-height: 1.625; }\n`;
|
|
644
|
+
css += `.leading-loose { line-height: 2; }\n`;
|
|
645
|
+
css += `.text-display { font-size: clamp(2.5rem, 6vw, 4rem); }\n`;
|
|
646
|
+
|
|
647
|
+
css += `.tracking-tighter { letter-spacing: -0.05em; }\n`;
|
|
648
|
+
css += `.tracking-tight { letter-spacing: -0.025em; }\n`;
|
|
649
|
+
css += `.tracking-normal { letter-spacing: 0em; }\n`;
|
|
650
|
+
css += `.tracking-wide { letter-spacing: 0.025em; }\n`;
|
|
651
|
+
css += `.tracking-wider { letter-spacing: 0.05em; }\n`;
|
|
652
|
+
css += `.tracking-widest { letter-spacing: 0.1em; }\n`;
|
|
653
|
+
|
|
654
|
+
css += `.underline { text-decoration-line: underline; }\n`;
|
|
655
|
+
css += `.overline { text-decoration-line: overline; }\n`;
|
|
656
|
+
css += `.line-through { text-decoration-line: line-through; }\n`;
|
|
657
|
+
css += `.no-underline { text-decoration-line: none; }\n`;
|
|
658
|
+
css += `.decoration-solid { text-decoration-style: solid; }\n`;
|
|
659
|
+
css += `.decoration-double { text-decoration-style: double; }\n`;
|
|
660
|
+
css += `.decoration-dotted { text-decoration-style: dotted; }\n`;
|
|
661
|
+
css += `.decoration-dashed { text-decoration-style: dashed; }\n`;
|
|
662
|
+
css += `.decoration-wavy { text-decoration-style: wavy; }\n`;
|
|
663
|
+
css += `.underline-offset-auto { text-underline-offset: auto; }\n`;
|
|
664
|
+
[0, 1, 2, 4, 8].forEach(value => {
|
|
665
|
+
css += `.underline-offset-${value} { text-underline-offset: ${value}px; }\n`;
|
|
666
|
+
});
|
|
667
|
+
css += `.decoration-auto { text-decoration-thickness: auto; }\n`;
|
|
668
|
+
css += `.decoration-from-font { text-decoration-thickness: from-font; }\n`;
|
|
669
|
+
[0, 1, 2, 4, 8].forEach(value => {
|
|
670
|
+
css += `.decoration-${value} { text-decoration-thickness: ${value}px; }\n`;
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
css += `.normal-nums { font-variant-numeric: normal; }\n`;
|
|
674
|
+
css += `.ordinal { font-variant-numeric: ordinal; }\n`;
|
|
675
|
+
css += `.slashed-zero { font-variant-numeric: slashed-zero; }\n`;
|
|
676
|
+
css += `.lining-nums { font-variant-numeric: lining-nums; }\n`;
|
|
677
|
+
css += `.oldstyle-nums { font-variant-numeric: oldstyle-nums; }\n`;
|
|
678
|
+
css += `.proportional-nums { font-variant-numeric: proportional-nums; }\n`;
|
|
679
|
+
css += `.tabular-nums { font-variant-numeric: tabular-nums; }\n`;
|
|
680
|
+
css += `.diagonal-fractions { font-variant-numeric: diagonal-fractions; }\n`;
|
|
681
|
+
css += `.stacked-fractions { font-variant-numeric: stacked-fractions; }\n`;
|
|
682
|
+
|
|
683
|
+
css += `.uppercase { text-transform: uppercase; }\n`;
|
|
684
|
+
css += `.lowercase { text-transform: lowercase; }\n`;
|
|
685
|
+
css += `.capitalize { text-transform: capitalize; }\n`;
|
|
686
|
+
css += `.normal-case { text-transform: none; }\n`;
|
|
687
|
+
|
|
688
|
+
css += `.font-sans { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n`;
|
|
689
|
+
css += `.font-serif { font-family: Georgia, "Times New Roman", serif; }\n`;
|
|
690
|
+
css += `.font-mono { font-family: "Menlo", "Monaco", "Courier New", monospace; }\n`;
|
|
691
|
+
css += `.font-inter { font-family: "Inter", system-ui, sans-serif; }\n`;
|
|
692
|
+
css += `.font-lexend { font-family: "Lexend", system-ui, sans-serif; }\n`;
|
|
693
|
+
css += `.font-dm-sans { font-family: "DM Sans", system-ui, sans-serif; }\n`;
|
|
694
|
+
css += `.font-nunito { font-family: "Nunito", system-ui, sans-serif; }\n`;
|
|
695
|
+
css += `.font-atkinson { font-family: "Atkinson Hyperlegible", system-ui, sans-serif; }\n`;
|
|
696
|
+
|
|
697
|
+
css += `\n`;
|
|
698
|
+
return css;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// BORDER UTILITIES
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
function generateBorderUtilities(config) {
|
|
706
|
+
let css = `/* Borders & Radius */\n`;
|
|
707
|
+
|
|
708
|
+
const borderWidths = config.spacing.borderWidths || [0, 2, 4, 8];
|
|
709
|
+
|
|
710
|
+
css += `.border { border-width: 1px; border-style: solid; }\n`;
|
|
711
|
+
borderWidths.forEach(width => {
|
|
712
|
+
css += `.border-${width} { border-width: ${width}px; }\n`;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const sides = {
|
|
716
|
+
t: 'top', r: 'right', b: 'bottom', l: 'left',
|
|
717
|
+
x: ['left', 'right'], y: ['top', 'bottom'],
|
|
718
|
+
s: 'inline-start', e: 'inline-end'
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
Object.entries(sides).forEach(([side, value]) => {
|
|
722
|
+
if (Array.isArray(value)) {
|
|
723
|
+
css += `.border-${side} { border-${value[0]}-width: 1px; border-${value[1]}-width: 1px; border-${value[0]}-style: solid; border-${value[1]}-style: solid; }\n`;
|
|
724
|
+
} else {
|
|
725
|
+
css += `.border-${side} { border-${value}-width: 1px; border-${value}-style: solid; }\n`;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
borderWidths.forEach(width => {
|
|
730
|
+
Object.entries(sides).forEach(([side, value]) => {
|
|
731
|
+
if (Array.isArray(value)) {
|
|
732
|
+
css += `.border-${side}-${width} { border-${value[0]}-width: ${width}px; border-${value[1]}-width: ${width}px; border-${value[0]}-style: solid; border-${value[1]}-style: solid; }\n`;
|
|
733
|
+
} else {
|
|
734
|
+
css += `.border-${side}-${width} { border-${value}-width: ${width}px; border-${value}-style: solid; }\n`;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
css += `.border-solid { border-style: solid; }\n`;
|
|
740
|
+
css += `.border-dashed { border-style: dashed; }\n`;
|
|
741
|
+
css += `.border-dotted { border-style: dotted; }\n`;
|
|
742
|
+
css += `.border-double { border-style: double; }\n`;
|
|
743
|
+
css += `.border-hidden { border-style: hidden; }\n`;
|
|
744
|
+
css += `.border-none { border-style: none; }\n`;
|
|
745
|
+
css += `.border-transparent { border-color: transparent; }\n`;
|
|
746
|
+
css += `.border-current { border-color: currentColor; }\n`;
|
|
747
|
+
css += `.border-black { border-color: #111110; }\n`;
|
|
748
|
+
css += `.border-white { border-color: #FAFAFA; }\n`;
|
|
749
|
+
|
|
750
|
+
const baseRadius = config.spacing.borderRadius['base'] || '8px';
|
|
751
|
+
css += `.rounded { border-radius: ${baseRadius}; }\n`;
|
|
752
|
+
|
|
753
|
+
Object.entries(config.spacing.borderRadius).forEach(([name, value]) => {
|
|
754
|
+
css += `.rounded-${name} { border-radius: ${value}; }\n`;
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const radiusTargets = {
|
|
758
|
+
t: ['top-left', 'top-right'], r: ['top-right', 'bottom-right'],
|
|
759
|
+
b: ['bottom-right', 'bottom-left'], l: ['top-left', 'bottom-left'],
|
|
760
|
+
tl: ['top-left'], tr: ['top-right'], br: ['bottom-right'], bl: ['bottom-left']
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
Object.entries(radiusTargets).forEach(([side, corners]) => {
|
|
764
|
+
corners.forEach(corner => {
|
|
765
|
+
css += `.rounded-${side} { border-${corner}-radius: ${baseRadius}; }\n`;
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
Object.entries(config.spacing.borderRadius).forEach(([name, value]) => {
|
|
770
|
+
Object.entries(radiusTargets).forEach(([side, corners]) => {
|
|
771
|
+
corners.forEach(corner => {
|
|
772
|
+
css += `.rounded-${side}-${name} { border-${corner}-radius: ${value}; }\n`;
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
css += `\n`;
|
|
778
|
+
return css;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ============================================================================
|
|
782
|
+
// BASE ELEMENT STYLES
|
|
783
|
+
// ============================================================================
|
|
784
|
+
|
|
785
|
+
function generateBaseStyles(config) {
|
|
786
|
+
const baseStyles = config.baseStyles;
|
|
787
|
+
if (!baseStyles || Object.keys(baseStyles).length === 0) return '';
|
|
788
|
+
|
|
789
|
+
// Build a lookup map from font size name → CSS variable
|
|
790
|
+
const fontSizeMap = {};
|
|
791
|
+
(config.typography?.fontSizes || []).forEach(({ name }) => {
|
|
792
|
+
fontSizeMap[name] = `var(--text-${name})`;
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Line height hints per size — keeps headings tighter than body text
|
|
796
|
+
const lineHeightMap = {
|
|
797
|
+
xs: '1.5', sm: '1.5', base: '1.6', lg: '1.6',
|
|
798
|
+
xl: '1.5', '2xl': '1.4', '3xl': '1.35', '4xl': '1.3',
|
|
799
|
+
'5xl': '1.15', '6xl': '1.1', '7xl': '1.05', '8xl': '1', '9xl': '1'
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
let css = '\n /* Base element styles (from baseStyles in emily.config.json) */\n';
|
|
803
|
+
Object.entries(baseStyles).forEach(([element, sizeKey]) => {
|
|
804
|
+
const varRef = fontSizeMap[sizeKey];
|
|
805
|
+
if (!varRef) return;
|
|
806
|
+
const lh = lineHeightMap[sizeKey] || '1.5';
|
|
807
|
+
css += ` ${element} { font-size: ${varRef}; line-height: ${lh}; }\n`;
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
return css;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ============================================================================
|
|
814
|
+
// COLOUR UTILITIES
|
|
815
|
+
// ============================================================================
|
|
816
|
+
|
|
817
|
+
function generateColourUtilities(colours) {
|
|
818
|
+
let css = `/* Colours: Background, Text, Borders, Accents */\n`;
|
|
819
|
+
// Uses CSS custom properties rather than hardcoded hex so colour utilities
|
|
820
|
+
// can be overridden via variable redefinition (e.g. dark mode, theme layers).
|
|
821
|
+
// The hex values are still declared as --color-* tokens in @layer theme.
|
|
822
|
+
|
|
823
|
+
Object.entries(colours).forEach(([colourName, shades]) => {
|
|
824
|
+
// Background colours
|
|
825
|
+
Object.entries(shades).forEach(([shade]) => {
|
|
826
|
+
css += `.bg-${colourName}-${shade} { background-color: var(--color-${colourName}-${shade}); }\n`;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Text colours
|
|
830
|
+
Object.entries(shades).forEach(([shade]) => {
|
|
831
|
+
css += `.text-${colourName}-${shade} { color: var(--color-${colourName}-${shade}); }\n`;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Border colours
|
|
835
|
+
Object.entries(shades).forEach(([shade]) => {
|
|
836
|
+
css += `.border-${colourName}-${shade} { border-color: var(--color-${colourName}-${shade}); }\n`;
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Accent colours (for form elements like checkboxes, radio buttons)
|
|
840
|
+
Object.entries(shades).forEach(([shade]) => {
|
|
841
|
+
css += `.accent-${colourName}-${shade} { accent-color: var(--color-${colourName}-${shade}); }\n`;
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
css += `.bg-white { background-color: #FAFAFA; }\n`;
|
|
846
|
+
css += `.bg-black { background-color: #111110; }\n`;
|
|
847
|
+
css += `.bg-transparent { background-color: transparent; }\n`;
|
|
848
|
+
css += `.bg-current { background-color: currentColor; }\n`;
|
|
849
|
+
|
|
850
|
+
css += `.text-white { color: #FAFAFA; }\n`;
|
|
851
|
+
css += `.text-black { color: #111110; }\n`;
|
|
852
|
+
css += `.text-transparent { color: transparent; }\n`;
|
|
853
|
+
css += `.text-current { color: currentColor; }\n`;
|
|
854
|
+
|
|
855
|
+
css += `\n`;
|
|
856
|
+
return css;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function generateSemanticColourUtilities(semanticColours) {
|
|
860
|
+
if (!semanticColours) return '';
|
|
861
|
+
let css = `/* Semantic colours: single value, no shade scale */\n`;
|
|
862
|
+
Object.entries(semanticColours).forEach(([name]) => {
|
|
863
|
+
css += `.bg-${name} { background-color: var(--color-${name}); }\n`;
|
|
864
|
+
css += `.text-${name} { color: var(--color-${name}); }\n`;
|
|
865
|
+
css += `.border-${name} { border-color: var(--color-${name}); }\n`;
|
|
866
|
+
css += `.fill-${name} { fill: var(--color-${name}); }\n`;
|
|
867
|
+
});
|
|
868
|
+
css += `\n`;
|
|
869
|
+
return css;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ============================================================================
|
|
873
|
+
// ARIA & DATA-STATE VARIANTS
|
|
874
|
+
// ============================================================================
|
|
875
|
+
// Generates ARIA attribute and data-state variants for all utility classes.
|
|
876
|
+
// Selectors target the attribute value directly so they work without JS —
|
|
877
|
+
// just toggle the attribute and the utility activates.
|
|
878
|
+
//
|
|
879
|
+
// Usage in HTML:
|
|
880
|
+
// aria-expanded: class="aria-expanded:block" aria-expanded="true"
|
|
881
|
+
// data-open: class="data-open:flex" data-state="open"
|
|
882
|
+
//
|
|
883
|
+
// Output examples:
|
|
884
|
+
// .aria-expanded\:block[aria-expanded="true"] { display: block; }
|
|
885
|
+
// .data-open\:flex[data-state="open"] { display: flex; }
|
|
886
|
+
|
|
887
|
+
function addAriaDataVariants(css) {
|
|
888
|
+
const variants = [
|
|
889
|
+
{ name: 'aria-expanded', selector: '[aria-expanded="true"]' },
|
|
890
|
+
{ name: 'aria-selected', selector: '[aria-selected="true"]' },
|
|
891
|
+
{ name: 'aria-checked', selector: '[aria-checked="true"]' },
|
|
892
|
+
{ name: 'aria-current', selector: '[aria-current="page"]' },
|
|
893
|
+
{ name: 'aria-disabled', selector: '[aria-disabled="true"]' },
|
|
894
|
+
{ name: 'data-open', selector: '[data-state="open"]' },
|
|
895
|
+
{ name: 'data-closed', selector: '[data-state="closed"]' },
|
|
896
|
+
{ name: 'data-checked', selector: '[data-state="checked"]' },
|
|
897
|
+
{ name: 'data-unchecked', selector: '[data-state="unchecked"]' },
|
|
898
|
+
{ name: 'data-active', selector: '[data-state="active"]' },
|
|
899
|
+
{ name: 'data-inactive', selector: '[data-state="inactive"]' },
|
|
900
|
+
];
|
|
901
|
+
|
|
902
|
+
let variantCss = css;
|
|
903
|
+
|
|
904
|
+
variants.forEach(variant => {
|
|
905
|
+
let variantRules = '';
|
|
906
|
+
const lines = css.split('\n');
|
|
907
|
+
|
|
908
|
+
lines.forEach(line => {
|
|
909
|
+
if (line.startsWith('.') && line.includes('{')) {
|
|
910
|
+
const className = line.split('{')[0].trim();
|
|
911
|
+
// Skip already-variant lines (contain ':' in class name) and special selectors
|
|
912
|
+
if (
|
|
913
|
+
!className.startsWith(':root') &&
|
|
914
|
+
!className.includes('@') &&
|
|
915
|
+
!className.includes('::') &&
|
|
916
|
+
!className.includes(':')
|
|
917
|
+
) {
|
|
918
|
+
const classWithoutDot = className.substring(1);
|
|
919
|
+
const ariaSelector = `.${variant.name}\\:${classWithoutDot}${variant.selector}`;
|
|
920
|
+
const ariaRule = line.replace(className, ariaSelector);
|
|
921
|
+
variantRules += ariaRule + '\n';
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
if (variantRules) {
|
|
927
|
+
variantCss += `\n/* ARIA/data-state variant: ${variant.name} */\n` + variantRules;
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
return variantCss;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// DARK MODE VARIANTS
|
|
936
|
+
// ============================================================================
|
|
937
|
+
// Generates dark: prefixed versions of colour and appearance utilities only.
|
|
938
|
+
// Layout, spacing, and typography utilities don't change in dark mode —
|
|
939
|
+
// targeting only the utilities where dark mode actually makes a difference
|
|
940
|
+
// keeps the output lean and the purge step effective.
|
|
941
|
+
//
|
|
942
|
+
// Usage in HTML: class="bg-neutral-10 dark:bg-neutral-90 text-neutral-90 dark:text-neutral-10"
|
|
943
|
+
// Output: @media (prefers-color-scheme: dark) { .dark\:bg-neutral-90 { background-color: ...; } }
|
|
944
|
+
|
|
945
|
+
function addDarkModeVariants(css) {
|
|
946
|
+
// Match on CSS property, not class name prefix — avoids catching
|
|
947
|
+
// structural utilities like text-xs (font-size) or text-left (text-align)
|
|
948
|
+
// when we only want colour-related declarations.
|
|
949
|
+
const colourProperties = [
|
|
950
|
+
'background-color',
|
|
951
|
+
'color',
|
|
952
|
+
'border-color',
|
|
953
|
+
'accent-color',
|
|
954
|
+
'box-shadow',
|
|
955
|
+
'opacity',
|
|
956
|
+
'fill',
|
|
957
|
+
'stroke',
|
|
958
|
+
'--tw-ring-color',
|
|
959
|
+
'outline-color',
|
|
960
|
+
];
|
|
961
|
+
|
|
962
|
+
let darkRules = '';
|
|
963
|
+
const lines = css.split('\n');
|
|
964
|
+
|
|
965
|
+
lines.forEach(line => {
|
|
966
|
+
if (line.startsWith('.') && line.includes('{') && line.includes('}')) {
|
|
967
|
+
const className = line.split('{')[0].trim();
|
|
968
|
+
|
|
969
|
+
// Only base utilities — skip anything already a variant (contains ':')
|
|
970
|
+
if (className.includes(':')) return;
|
|
971
|
+
|
|
972
|
+
// Only colour/appearance properties
|
|
973
|
+
const isColourUtility = colourProperties.some(prop => line.includes(prop + ':'));
|
|
974
|
+
if (!isColourUtility) return;
|
|
975
|
+
|
|
976
|
+
const classWithoutDot = className.substring(1);
|
|
977
|
+
const darkRule = line.replace(className, `.dark\\:${classWithoutDot}`);
|
|
978
|
+
darkRules += ' ' + darkRule + '\n';
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (!darkRules) return css;
|
|
983
|
+
|
|
984
|
+
return css
|
|
985
|
+
+ `\n/* Dark mode variants — explicit override */\n[data-theme="dark"] {\n${darkRules}}\n`
|
|
986
|
+
+ `\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`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ============================================================================
|
|
990
|
+
// RESPONSIVE VARIANTS
|
|
991
|
+
// ============================================================================
|
|
992
|
+
|
|
993
|
+
function addResponsiveVariants(css, config) {
|
|
994
|
+
let variantCss = css;
|
|
995
|
+
|
|
996
|
+
Object.entries(config.breakpoints).forEach(([breakpointName, breakpointValue]) => {
|
|
997
|
+
const mediaQuery = `@media (min-width: ${breakpointValue}) {\n`;
|
|
998
|
+
let breakpointRules = '';
|
|
999
|
+
|
|
1000
|
+
// Extract all utility rules and add responsive prefix
|
|
1001
|
+
const lines = css.split('\n');
|
|
1002
|
+
lines.forEach(line => {
|
|
1003
|
+
if (line.startsWith('.') && line.includes('{')) {
|
|
1004
|
+
const className = line.split('{')[0].trim();
|
|
1005
|
+
const rule = line;
|
|
1006
|
+
// Skip variables and already responsive selectors
|
|
1007
|
+
if (!className.startsWith(':root') && !className.includes(':')) {
|
|
1008
|
+
const responsiveRule = rule.replace(className, `.${breakpointName}\\:${className.substring(1)}`);
|
|
1009
|
+
breakpointRules += ' ' + responsiveRule + '\n';
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
if (breakpointRules) {
|
|
1015
|
+
variantCss += mediaQuery + breakpointRules + '}\n\n';
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
return variantCss;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
// STATE VARIANTS
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
// Add pseudo-class variants for hover, focus-visible, active, disabled
|
|
1026
|
+
|
|
1027
|
+
function addStateVariants(css) {
|
|
1028
|
+
const states = [
|
|
1029
|
+
{ name: 'hover', selector: ':hover' },
|
|
1030
|
+
{ name: 'focus', selector: ':focus' },
|
|
1031
|
+
{ name: 'focus-within', selector: ':focus-within' },
|
|
1032
|
+
{ name: 'focus-visible', selector: ':focus-visible' },
|
|
1033
|
+
{ name: 'active', selector: ':active' },
|
|
1034
|
+
{ name: 'disabled', selector: ':disabled' }
|
|
1035
|
+
];
|
|
1036
|
+
|
|
1037
|
+
let variantCss = css;
|
|
1038
|
+
|
|
1039
|
+
states.forEach(state => {
|
|
1040
|
+
let stateRules = '';
|
|
1041
|
+
|
|
1042
|
+
// Extract all utility rules and add state prefix
|
|
1043
|
+
const lines = css.split('\n');
|
|
1044
|
+
lines.forEach(line => {
|
|
1045
|
+
if (line.startsWith('.') && line.includes('{')) {
|
|
1046
|
+
const className = line.split('{')[0].trim();
|
|
1047
|
+
// Skip variables, media queries, pseudo-elements, and state variants
|
|
1048
|
+
if (!className.startsWith(':root') && !className.includes('@') && !className.includes('::') && !className.includes(':')) {
|
|
1049
|
+
// Generate state variant: .hover\:block:hover { display: block; }
|
|
1050
|
+
// Remove leading dot from className, add state prefix with escaped colon
|
|
1051
|
+
const classWithoutDot = className.substring(1);
|
|
1052
|
+
const stateSelector = `.${state.name}\\:${classWithoutDot}${state.selector}`;
|
|
1053
|
+
const statefulRule = line.replace(className, stateSelector);
|
|
1054
|
+
stateRules += statefulRule + '\n';
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
if (stateRules) {
|
|
1060
|
+
variantCss += '\n/* State variant: ' + state.name + ' */\n' + stateRules;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
return variantCss;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
// ============================================================================
|
|
1069
|
+
// PATTERN COMPONENTS
|
|
1070
|
+
// ============================================================================
|
|
1071
|
+
// Composite classes that combine multiple utilities into named patterns.
|
|
1072
|
+
// These live in @layer components so utilities always take precedence in the cascade.
|
|
1073
|
+
// Gap values reference spacing variables generated from emily.config.json,
|
|
1074
|
+
// with pixel fallbacks so they work even without the variables in scope.
|
|
1075
|
+
|
|
1076
|
+
function generatePatternComponents() {
|
|
1077
|
+
return `
|
|
1078
|
+
/* ---- Centering ---- */
|
|
1079
|
+
|
|
1080
|
+
/* Full-viewport overlay centering — use for modals, lightboxes, toasts */
|
|
1081
|
+
.center-screen {
|
|
1082
|
+
position: fixed;
|
|
1083
|
+
inset: 0;
|
|
1084
|
+
display: flex;
|
|
1085
|
+
align-items: center;
|
|
1086
|
+
justify-content: center;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/* Transform-based centering within a relative/absolute parent */
|
|
1090
|
+
.center-absolute {
|
|
1091
|
+
position: absolute;
|
|
1092
|
+
top: 50%;
|
|
1093
|
+
left: 50%;
|
|
1094
|
+
transform: translate(-50%, -50%);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/* ---- Reading / Prose ---- */
|
|
1098
|
+
|
|
1099
|
+
/* Comfortable reading column — limits line length, centers the block */
|
|
1100
|
+
.prose {
|
|
1101
|
+
max-width: 65ch;
|
|
1102
|
+
margin-inline: auto;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.prose-emily {
|
|
1106
|
+
max-width: 65ch;
|
|
1107
|
+
margin-inline: auto;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.prose-emily > * + * {
|
|
1111
|
+
margin-top: var(--space-4, 1rem);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.prose-emily h2,
|
|
1115
|
+
.prose-emily h3 {
|
|
1116
|
+
font-family: inherit;
|
|
1117
|
+
color: var(--color-neutral-90);
|
|
1118
|
+
line-height: 1.25;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.prose-emily h2 {
|
|
1122
|
+
font-size: var(--text-2xl, 24px);
|
|
1123
|
+
margin-top: var(--space-10, 2.5rem);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.prose-emily h3 {
|
|
1127
|
+
font-size: var(--text-xl, 20px);
|
|
1128
|
+
margin-top: var(--space-8, 2rem);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.prose-emily p,
|
|
1132
|
+
.prose-emily li {
|
|
1133
|
+
color: var(--color-neutral-70);
|
|
1134
|
+
line-height: 1.75;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.prose-emily ul,
|
|
1138
|
+
.prose-emily ol {
|
|
1139
|
+
padding-left: var(--space-6, 1.5rem);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.prose-emily ul {
|
|
1143
|
+
list-style-type: disc;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
.prose-emily ol {
|
|
1147
|
+
list-style-type: decimal;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.prose-emily a {
|
|
1151
|
+
color: var(--color-brand-80);
|
|
1152
|
+
text-decoration: underline;
|
|
1153
|
+
text-underline-offset: 2px;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.prose-emily code {
|
|
1157
|
+
font-size: var(--text-sm, 14px);
|
|
1158
|
+
background-color: var(--color-neutral-10);
|
|
1159
|
+
border: 1px solid var(--color-neutral-20);
|
|
1160
|
+
border-radius: var(--space-1, 0.25rem);
|
|
1161
|
+
padding: 0.125rem 0.375rem;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/* ---- Composition ---- */
|
|
1165
|
+
|
|
1166
|
+
/* Vertical stack with consistent gap — replaces manual margin chains */
|
|
1167
|
+
.stack {
|
|
1168
|
+
display: flex;
|
|
1169
|
+
flex-direction: column;
|
|
1170
|
+
gap: var(--space-4, 1rem);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/* Horizontal grouping with wrapping — for tags, button rows, icon lists */
|
|
1174
|
+
.cluster {
|
|
1175
|
+
display: flex;
|
|
1176
|
+
flex-wrap: wrap;
|
|
1177
|
+
gap: var(--space-4, 1rem);
|
|
1178
|
+
align-items: center;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* ---- Layout ---- */
|
|
1182
|
+
|
|
1183
|
+
/* Constrained width container — 1100px max, full-width on small screens */
|
|
1184
|
+
.width-container {
|
|
1185
|
+
width: 100%;
|
|
1186
|
+
max-width: 1100px;
|
|
1187
|
+
margin-inline: auto;
|
|
1188
|
+
padding-inline: var(--space-4, 1rem);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
@media (min-width: 640px) {
|
|
1192
|
+
.width-container {
|
|
1193
|
+
padding-inline: var(--space-6, 1.5rem);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
@media (min-width: 1024px) {
|
|
1198
|
+
.width-container {
|
|
1199
|
+
padding-inline: var(--space-8, 2rem);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@media (min-width: 1140px) {
|
|
1204
|
+
.width-container {
|
|
1205
|
+
padding-inline: 0;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* ---- Forms ---- */
|
|
1210
|
+
|
|
1211
|
+
.field-container {
|
|
1212
|
+
display: flex;
|
|
1213
|
+
flex-direction: column;
|
|
1214
|
+
gap: var(--space-2, 0.5rem);
|
|
1215
|
+
margin-bottom: var(--space-6, 1.5rem);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.field-container label {
|
|
1219
|
+
display: block;
|
|
1220
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1221
|
+
color: var(--color-neutral-90);
|
|
1222
|
+
font-size: var(--text-base, 16px);
|
|
1223
|
+
line-height: 1.4;
|
|
1224
|
+
margin-bottom: var(--space-1, 0.25rem);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
fieldset {
|
|
1228
|
+
border: none;
|
|
1229
|
+
padding: 0;
|
|
1230
|
+
margin: 0 0 var(--space-6, 1.5rem);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
fieldset legend {
|
|
1234
|
+
display: block;
|
|
1235
|
+
font-size: var(--text-lg, 18px);
|
|
1236
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1237
|
+
margin-bottom: var(--space-3, 0.75rem);
|
|
1238
|
+
color: var(--color-neutral-90);
|
|
1239
|
+
padding: 0;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.form-hint {
|
|
1243
|
+
font-size: var(--text-sm, 14px);
|
|
1244
|
+
color: var(--color-neutral-60);
|
|
1245
|
+
margin-bottom: var(--space-1, 0.25rem);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
input[type="text"],
|
|
1249
|
+
input[type="email"],
|
|
1250
|
+
input[type="password"],
|
|
1251
|
+
input[type="number"],
|
|
1252
|
+
input[type="tel"],
|
|
1253
|
+
input[type="url"],
|
|
1254
|
+
input[type="search"],
|
|
1255
|
+
input[type="date"],
|
|
1256
|
+
select,
|
|
1257
|
+
textarea {
|
|
1258
|
+
width: 100%;
|
|
1259
|
+
max-width: 100%;
|
|
1260
|
+
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
|
|
1261
|
+
border: 2px solid var(--color-neutral-30);
|
|
1262
|
+
border-radius: 8px;
|
|
1263
|
+
background-color: #ffffff;
|
|
1264
|
+
color: var(--color-neutral-90);
|
|
1265
|
+
font-family: inherit;
|
|
1266
|
+
font-size: var(--text-base, 16px);
|
|
1267
|
+
line-height: var(--leading-base, 1.6);
|
|
1268
|
+
appearance: none;
|
|
1269
|
+
transition: border-color 200ms ease, box-shadow 200ms ease;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
select {
|
|
1273
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
1274
|
+
background-position: right var(--space-2, 0.5rem) center;
|
|
1275
|
+
background-repeat: no-repeat;
|
|
1276
|
+
background-size: 1.5em 1.5em;
|
|
1277
|
+
padding-right: var(--space-10, 2.5rem);
|
|
1278
|
+
cursor: pointer;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
textarea {
|
|
1282
|
+
min-height: 120px;
|
|
1283
|
+
resize: vertical;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
input[type="text"]:focus,
|
|
1287
|
+
input[type="email"]:focus,
|
|
1288
|
+
input[type="password"]:focus,
|
|
1289
|
+
input[type="number"]:focus,
|
|
1290
|
+
input[type="tel"]:focus,
|
|
1291
|
+
input[type="url"]:focus,
|
|
1292
|
+
input[type="search"]:focus,
|
|
1293
|
+
input[type="date"]:focus,
|
|
1294
|
+
select:focus,
|
|
1295
|
+
textarea:focus {
|
|
1296
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1297
|
+
outline-offset: 3px;
|
|
1298
|
+
border-color: var(--color-neutral-80);
|
|
1299
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.checkbox-group,
|
|
1303
|
+
.radio-group {
|
|
1304
|
+
display: flex;
|
|
1305
|
+
align-items: center;
|
|
1306
|
+
gap: var(--space-3, 0.75rem);
|
|
1307
|
+
margin-bottom: var(--space-4, 1rem);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.checkbox-group label,
|
|
1311
|
+
.radio-group label {
|
|
1312
|
+
font-weight: var(--font-weight-normal, 400);
|
|
1313
|
+
margin-bottom: 0;
|
|
1314
|
+
cursor: pointer;
|
|
1315
|
+
font-size: var(--text-base, 16px);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
input[type="checkbox"] {
|
|
1319
|
+
width: 1.5rem;
|
|
1320
|
+
height: 1.5rem;
|
|
1321
|
+
margin: 0;
|
|
1322
|
+
cursor: pointer;
|
|
1323
|
+
accent-color: var(--color-brand-80);
|
|
1324
|
+
flex-shrink: 0;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
input[type="checkbox"]:focus {
|
|
1328
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1329
|
+
outline-offset: 3px;
|
|
1330
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
input[type="radio"] {
|
|
1334
|
+
width: 1.5rem;
|
|
1335
|
+
height: 1.5rem;
|
|
1336
|
+
margin: 0;
|
|
1337
|
+
border-radius: 50%;
|
|
1338
|
+
appearance: none;
|
|
1339
|
+
background-color: #ffffff;
|
|
1340
|
+
border: 2px solid var(--color-neutral-30);
|
|
1341
|
+
display: grid;
|
|
1342
|
+
place-content: center;
|
|
1343
|
+
cursor: pointer;
|
|
1344
|
+
flex-shrink: 0;
|
|
1345
|
+
transition: background-color 200ms ease, border-color 200ms ease;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
input[type="radio"]::before {
|
|
1349
|
+
content: "";
|
|
1350
|
+
width: 0.75rem;
|
|
1351
|
+
height: 0.75rem;
|
|
1352
|
+
border-radius: 50%;
|
|
1353
|
+
transform: scale(0);
|
|
1354
|
+
transition: 120ms transform ease-in-out;
|
|
1355
|
+
background-color: var(--color-brand-80);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
input[type="radio"]:checked {
|
|
1359
|
+
border-color: var(--color-brand-80);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
input[type="radio"]:checked::before {
|
|
1363
|
+
transform: scale(1);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
input[type="radio"]:hover {
|
|
1367
|
+
background-color: var(--color-brand-10);
|
|
1368
|
+
border-color: var(--color-brand-80);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
input[type="radio"]:focus {
|
|
1372
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1373
|
+
outline-offset: 3px;
|
|
1374
|
+
border-radius: 50%;
|
|
1375
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
input[aria-invalid="true"] {
|
|
1379
|
+
border-color: var(--color-error-80) !important;
|
|
1380
|
+
border-width: 3px;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.form-error-message {
|
|
1384
|
+
font-size: var(--text-sm, 14px);
|
|
1385
|
+
font-weight: var(--font-weight-bold, 700);
|
|
1386
|
+
color: var(--color-error-80);
|
|
1387
|
+
margin-top: var(--space-1, 0.25rem);
|
|
1388
|
+
display: block;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.error-summary {
|
|
1392
|
+
border: 4px solid var(--color-error-80);
|
|
1393
|
+
padding: var(--space-6, 1.5rem);
|
|
1394
|
+
margin-bottom: var(--space-8, 2rem);
|
|
1395
|
+
border-radius: 8px;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.error-summary ul {
|
|
1399
|
+
list-style: disc;
|
|
1400
|
+
padding-left: var(--space-5, 1.25rem);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
.error-summary a {
|
|
1404
|
+
color: var(--color-error-80);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/* ---- Buttons ---- */
|
|
1408
|
+
|
|
1409
|
+
.btn {
|
|
1410
|
+
display: inline-flex;
|
|
1411
|
+
align-items: center;
|
|
1412
|
+
justify-content: center;
|
|
1413
|
+
padding: var(--space-3, 0.75rem) var(--space-6, 1.5rem);
|
|
1414
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1415
|
+
border-radius: 8px;
|
|
1416
|
+
cursor: pointer;
|
|
1417
|
+
transition: background-color 200ms ease, border-color 200ms ease, color 200ms ease;
|
|
1418
|
+
border: 2px solid transparent;
|
|
1419
|
+
text-align: center;
|
|
1420
|
+
min-height: 3rem;
|
|
1421
|
+
font-size: var(--text-base, 16px);
|
|
1422
|
+
text-decoration: none;
|
|
1423
|
+
font-family: inherit;
|
|
1424
|
+
line-height: 1;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
.btn-primary {
|
|
1428
|
+
background-color: var(--color-brand-80);
|
|
1429
|
+
color: #ffffff;
|
|
1430
|
+
border-color: transparent;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.btn-primary:hover {
|
|
1434
|
+
background-color: var(--color-brand-90);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
.btn-primary:focus-visible {
|
|
1438
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1439
|
+
outline-offset: 3px;
|
|
1440
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
.btn-secondary {
|
|
1444
|
+
background-color: #ffffff;
|
|
1445
|
+
color: var(--color-accent-80);
|
|
1446
|
+
border-color: var(--color-accent-80);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
.btn-secondary:hover {
|
|
1450
|
+
background-color: var(--color-accent-10);
|
|
1451
|
+
color: var(--color-accent-90);
|
|
1452
|
+
border-color: var(--color-accent-90);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.btn-secondary:focus-visible {
|
|
1456
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1457
|
+
outline-offset: 3px;
|
|
1458
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.btn-ghost {
|
|
1462
|
+
background-color: transparent;
|
|
1463
|
+
color: var(--color-neutral-80);
|
|
1464
|
+
border-color: transparent;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.btn-ghost:hover {
|
|
1468
|
+
background-color: var(--color-neutral-10);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.btn-ghost:focus-visible {
|
|
1472
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1473
|
+
outline-offset: 3px;
|
|
1474
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.btn-danger {
|
|
1478
|
+
background-color: var(--color-error-80);
|
|
1479
|
+
color: #ffffff;
|
|
1480
|
+
border-color: transparent;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.btn-danger:hover {
|
|
1484
|
+
background-color: var(--color-error-90);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.btn-danger:focus-visible {
|
|
1488
|
+
outline: 2px solid var(--color-neutral-80);
|
|
1489
|
+
outline-offset: 3px;
|
|
1490
|
+
box-shadow: 0 0 0 4px var(--focus-ring-glow, rgba(219, 39, 119, 0.1));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.btn-sm {
|
|
1494
|
+
padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
|
|
1495
|
+
font-size: var(--text-sm, 14px);
|
|
1496
|
+
min-height: 2.25rem;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.btn-lg {
|
|
1500
|
+
padding: var(--space-4, 1rem) var(--space-8, 2rem);
|
|
1501
|
+
font-size: var(--text-lg, 18px);
|
|
1502
|
+
min-height: 3.5rem;
|
|
1503
|
+
}
|
|
1504
|
+
`;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// ============================================================================
|
|
1508
|
+
// BUILD FUNCTION
|
|
1509
|
+
// ============================================================================
|
|
1510
|
+
|
|
1511
|
+
function buildFullFramework() {
|
|
1512
|
+
const config = getConfig();
|
|
1513
|
+
|
|
1514
|
+
console.log('Building EmilyCSS full framework...');
|
|
1515
|
+
|
|
1516
|
+
const colours = generateAllColours(config.colours);
|
|
1517
|
+
console.log(`✓ Generated ${Object.keys(colours).length} colour scales`);
|
|
1518
|
+
if (config.semanticColours) {
|
|
1519
|
+
console.log(`✓ Generated ${Object.keys(config.semanticColours).length} semantic colour tokens`);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const spacing = generateSpacing(config.baseUnit, config.spacing.scale);
|
|
1523
|
+
console.log(`✓ Generated ${Object.keys(spacing).length} spacing values`);
|
|
1524
|
+
|
|
1525
|
+
const variablesCss = generateCSSVariables(colours, spacing, config);
|
|
1526
|
+
|
|
1527
|
+
let utilityCss = '';
|
|
1528
|
+
utilityCss += displayUtilities();
|
|
1529
|
+
utilityCss += generateSpacingUtilities(spacing);
|
|
1530
|
+
utilityCss += generateFlexboxUtilities(spacing);
|
|
1531
|
+
utilityCss += generateGridUtilities(spacing);
|
|
1532
|
+
utilityCss += sizingUtilities(spacing);
|
|
1533
|
+
utilityCss += generateTypographyUtilities(config);
|
|
1534
|
+
utilityCss += generateBorderUtilities(config);
|
|
1535
|
+
utilityCss += generateColourUtilities(colours);
|
|
1536
|
+
utilityCss += generateSemanticColourUtilities(config.semanticColours);
|
|
1537
|
+
utilityCss += positioningUtilities(spacing);
|
|
1538
|
+
utilityCss += overflowUtilities();
|
|
1539
|
+
utilityCss += transformUtilities(spacing);
|
|
1540
|
+
utilityCss += shadowUtilities();
|
|
1541
|
+
utilityCss += ringUtilities(colours);
|
|
1542
|
+
utilityCss += objectUtilities();
|
|
1543
|
+
utilityCss += tableListUtilities();
|
|
1544
|
+
utilityCss += svgUtilities(colours);
|
|
1545
|
+
utilityCss += formUtilities();
|
|
1546
|
+
utilityCss += verticalAlignUtilities();
|
|
1547
|
+
utilityCss += contentScrollUtilities();
|
|
1548
|
+
utilityCss += opacityUtilities();
|
|
1549
|
+
utilityCss += transitionUtilities();
|
|
1550
|
+
utilityCss += blendUtilities();
|
|
1551
|
+
utilityCss += cursorUtilities();
|
|
1552
|
+
utilityCss += accessibilityUtilities();
|
|
1553
|
+
utilityCss += containerUtilities();
|
|
1554
|
+
utilityCss += codeUtilities();
|
|
1555
|
+
utilityCss += animationUtilities();
|
|
1556
|
+
utilityCss += backdropUtilities();
|
|
1557
|
+
utilityCss += spaceUtilities(spacing);
|
|
1558
|
+
utilityCss += divideUtilities(spacing, colours);
|
|
1559
|
+
utilityCss += backgroundUtilities();
|
|
1560
|
+
utilityCss += filterUtilities();
|
|
1561
|
+
|
|
1562
|
+
utilityCss = addStateVariants(utilityCss);
|
|
1563
|
+
utilityCss = addAriaDataVariants(utilityCss);
|
|
1564
|
+
utilityCss = addDarkModeVariants(utilityCss);
|
|
1565
|
+
utilityCss = addResponsiveVariants(utilityCss, config);
|
|
1566
|
+
|
|
1567
|
+
const { fontFace, bodyFont } = generateFontCSS(config);
|
|
1568
|
+
|
|
1569
|
+
const fontLabel = typeof config.fontFamily === 'object'
|
|
1570
|
+
? 'heading: ' + (config.fontFamily.heading || 'system') + ', body: ' + (config.fontFamily.body || 'system')
|
|
1571
|
+
: (config.fontFamily || 'system');
|
|
1572
|
+
|
|
1573
|
+
console.log('✓ Font: ' + fontLabel);
|
|
1574
|
+
|
|
1575
|
+
const htmlFontSize = (config.baseFontSize && config.baseFontSize !== "16px")
|
|
1576
|
+
? `\n html { font-size: ${config.baseFontSize}; }\n`
|
|
1577
|
+
: "";
|
|
1578
|
+
|
|
1579
|
+
const baseCss = `${htmlFontSize}
|
|
1580
|
+
/* Box sizing */
|
|
1581
|
+
*, *::before, *::after {
|
|
1582
|
+
box-sizing: border-box;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
body, h1, h2, h3, h4, h5, h6, p,
|
|
1586
|
+
ul, ol, dl, dd, figure, blockquote,
|
|
1587
|
+
fieldset, textarea, pre {
|
|
1588
|
+
margin: 0;
|
|
1589
|
+
padding: 0;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
ul, ol {
|
|
1593
|
+
list-style: none;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
input, button, textarea, select {
|
|
1597
|
+
font: inherit;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
img, picture, video, canvas, svg {
|
|
1601
|
+
display: block;
|
|
1602
|
+
max-width: 100%;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
button {
|
|
1606
|
+
background: none;
|
|
1607
|
+
border: none;
|
|
1608
|
+
cursor: pointer;
|
|
1609
|
+
padding: 0;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
p, h1, h2, h3, h4, h5, h6 {
|
|
1613
|
+
overflow-wrap: break-word;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/* Base heading scale */
|
|
1617
|
+
h1 {
|
|
1618
|
+
font-size: var(--text-4xl, 36px);
|
|
1619
|
+
line-height: var(--leading-4xl, 1.3);
|
|
1620
|
+
font-weight: var(--font-weight-bold, 700);
|
|
1621
|
+
margin-bottom: var(--space-6, 1.5rem);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
h2 {
|
|
1625
|
+
font-size: var(--text-3xl, 30px);
|
|
1626
|
+
line-height: var(--leading-3xl, 1.4);
|
|
1627
|
+
font-weight: var(--font-weight-bold, 700);
|
|
1628
|
+
margin-bottom: var(--space-5, 1.25rem);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
h3 {
|
|
1632
|
+
font-size: var(--text-2xl, 24px);
|
|
1633
|
+
line-height: var(--leading-2xl, 1.4);
|
|
1634
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1635
|
+
margin-bottom: var(--space-4, 1rem);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
h4 {
|
|
1639
|
+
font-size: var(--text-xl, 20px);
|
|
1640
|
+
line-height: var(--leading-xl, 1.6);
|
|
1641
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1642
|
+
margin-bottom: var(--space-3, 0.75rem);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
h5 {
|
|
1646
|
+
font-size: var(--text-lg, 18px);
|
|
1647
|
+
line-height: var(--leading-lg, 1.6);
|
|
1648
|
+
font-weight: var(--font-weight-medium, 500);
|
|
1649
|
+
margin-bottom: var(--space-3, 0.75rem);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
h6 {
|
|
1653
|
+
font-size: var(--text-base, 16px);
|
|
1654
|
+
line-height: var(--leading-base, 1.6);
|
|
1655
|
+
font-weight: var(--font-weight-medium, 500);
|
|
1656
|
+
margin-bottom: var(--space-2, 0.5rem);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
p {
|
|
1660
|
+
font-size: var(--text-base, 16px);
|
|
1661
|
+
line-height: var(--leading-base, 1.6);
|
|
1662
|
+
margin-bottom: var(--space-4, 1rem);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
p:last-child {
|
|
1666
|
+
margin-bottom: 0;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
code {
|
|
1670
|
+
font-family: "Menlo", "Monaco", "Courier New", monospace;
|
|
1671
|
+
font-size: 0.875em;
|
|
1672
|
+
background-color: #0d0c0b;
|
|
1673
|
+
color: #e6ffd2;
|
|
1674
|
+
padding: 0.125rem 0.4rem;
|
|
1675
|
+
border-radius: 4px;
|
|
1676
|
+
display: inline;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
code.block {
|
|
1680
|
+
display: block;
|
|
1681
|
+
padding: 0.625rem 1rem;
|
|
1682
|
+
border-radius: 6px;
|
|
1683
|
+
font-size: 0.8125rem;
|
|
1684
|
+
line-height: 1.6;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
pre {
|
|
1688
|
+
background-color: #0d0c0b;
|
|
1689
|
+
color: #e4e0db;
|
|
1690
|
+
padding: 1.25rem;
|
|
1691
|
+
border-radius: 0 0 6px;
|
|
1692
|
+
overflow-x: auto;
|
|
1693
|
+
font-family: "Menlo", "Monaco", "Courier New", monospace;
|
|
1694
|
+
font-size: 0.875rem;
|
|
1695
|
+
line-height: 1.7;
|
|
1696
|
+
border: 1px solid #2a2520;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
pre code {
|
|
1700
|
+
background: none;
|
|
1701
|
+
padding: 0;
|
|
1702
|
+
border-radius: 0;
|
|
1703
|
+
color: inherit;
|
|
1704
|
+
font-size: inherit;
|
|
1705
|
+
font-family: inherit;
|
|
1706
|
+
display: inline;
|
|
1707
|
+
}
|
|
1708
|
+
${bodyFont}`;
|
|
1709
|
+
|
|
1710
|
+
let css = fontFace ? `${fontFace}\n` : '';
|
|
1711
|
+
css += `@layer theme, base, components, utilities;\n\n`;
|
|
1712
|
+
css += `@layer theme {\n${variablesCss}}\n\n`;
|
|
1713
|
+
|
|
1714
|
+
const baseStylesCss = generateBaseStyles(config);
|
|
1715
|
+
css += `@layer base {${baseCss}${baseStylesCss}}\n\n`;
|
|
1716
|
+
css += `@layer components {\n${generatePatternComponents()}}\n\n`;
|
|
1717
|
+
css += `@layer utilities {\n${utilityCss}}\n`;
|
|
1718
|
+
|
|
1719
|
+
const fullCssPath = getFullCssPath(config);
|
|
1720
|
+
|
|
1721
|
+
ensureDirectoryForFile(fullCssPath);
|
|
1722
|
+
fs.writeFileSync(fullCssPath, css);
|
|
1723
|
+
|
|
1724
|
+
const manifestSettings = getManifestSettings(config);
|
|
1725
|
+
const intellisenseSettings = getIntellisenseSettings(config);
|
|
1726
|
+
const shouldGenerateManifestData =
|
|
1727
|
+
manifestSettings.enabled || intellisenseSettings.enabled;
|
|
1728
|
+
const manifestData = shouldGenerateManifestData
|
|
1729
|
+
? generateManifest(css, config)
|
|
1730
|
+
: null;
|
|
1731
|
+
|
|
1732
|
+
if (manifestSettings.enabled) {
|
|
1733
|
+
const manifestPath = getManifestOutputPath(config);
|
|
1734
|
+
|
|
1735
|
+
ensureDirectoryForFile(manifestPath);
|
|
1736
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
|
|
1737
|
+
console.log(`✓ Generated manifest: ${manifestPath}`);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (intellisenseSettings.enabled) {
|
|
1741
|
+
const intellisensePath = getIntellisenseOutputPath(config);
|
|
1742
|
+
const intellisense = generateIntellisense(manifestData);
|
|
1743
|
+
|
|
1744
|
+
ensureDirectoryForFile(intellisensePath);
|
|
1745
|
+
fs.writeFileSync(intellisensePath, JSON.stringify(intellisense, null, 2));
|
|
1746
|
+
console.log(`✓ Generated IntelliSense: ${intellisensePath}`);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
console.log(`✓ Generated CSS: ${fullCssPath}`);
|
|
1750
|
+
console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
|
|
1751
|
+
console.log('Full framework build complete');
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function minify(css) {
|
|
1755
|
+
return css
|
|
1756
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
1757
|
+
.replace(/\s+/g, ' ')
|
|
1758
|
+
.replace(/\s?\{/g, '{')
|
|
1759
|
+
.replace(/\s?\}/g, '}')
|
|
1760
|
+
.replace(/;\s/g, ';')
|
|
1761
|
+
.trim();
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function buildProductionCss(options = {}) {
|
|
1765
|
+
const config = getConfig();
|
|
1766
|
+
const sourceDir = getSourceDir(config);
|
|
1767
|
+
const fullCssPath = getFullCssPath(config);
|
|
1768
|
+
const productionCssPath = getProductionCssPath(config);
|
|
1769
|
+
const profile = {
|
|
1770
|
+
purge: 0,
|
|
1771
|
+
minify: 0,
|
|
1772
|
+
write: 0,
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
if (!fs.existsSync(fullCssPath)) {
|
|
1776
|
+
buildFullFramework();
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const { purgeCSS } = require('./purge.js');
|
|
1780
|
+
const css = fs.readFileSync(fullCssPath, 'utf8');
|
|
1781
|
+
const purgeStart = Date.now();
|
|
1782
|
+
const purged = purgeCSS(css, sourceDir, config);
|
|
1783
|
+
profile.purge = Date.now() - purgeStart;
|
|
1784
|
+
|
|
1785
|
+
const minifyStart = Date.now();
|
|
1786
|
+
const minified = minify(purged);
|
|
1787
|
+
profile.minify = Date.now() - minifyStart;
|
|
1788
|
+
|
|
1789
|
+
const writeStart = Date.now();
|
|
1790
|
+
ensureDirectoryForFile(productionCssPath);
|
|
1791
|
+
fs.writeFileSync(productionCssPath, minified);
|
|
1792
|
+
profile.write = Date.now() - writeStart;
|
|
1793
|
+
|
|
1794
|
+
return {
|
|
1795
|
+
css,
|
|
1796
|
+
purged,
|
|
1797
|
+
minified,
|
|
1798
|
+
originalSize: Buffer.byteLength(css, 'utf8'),
|
|
1799
|
+
outputSize: Buffer.byteLength(minified, 'utf8'),
|
|
1800
|
+
outputPath: productionCssPath,
|
|
1801
|
+
fullCssPath,
|
|
1802
|
+
profile: options.profile ? profile : undefined,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function isFrameworkStale() {
|
|
1807
|
+
const config = getConfig();
|
|
1808
|
+
const configPath = getConfigPath();
|
|
1809
|
+
const fullCssPath = getFullCssPath(config);
|
|
1810
|
+
|
|
1811
|
+
if (!fs.existsSync(fullCssPath)) return true;
|
|
1812
|
+
if (!fs.existsSync(configPath)) return true;
|
|
1813
|
+
|
|
1814
|
+
return fs.statSync(configPath).mtimeMs > fs.statSync(fullCssPath).mtimeMs;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function ensureFullFramework() {
|
|
1818
|
+
if (isFrameworkStale()) {
|
|
1819
|
+
buildFullFramework();
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function build(options = {}) {
|
|
1824
|
+
const totalStart = Date.now();
|
|
1825
|
+
const frameworkStart = Date.now();
|
|
1826
|
+
ensureFullFramework();
|
|
1827
|
+
const frameworkMs = Date.now() - frameworkStart;
|
|
1828
|
+
|
|
1829
|
+
const config = getConfig();
|
|
1830
|
+
const fullCssPath = getFullCssPath(config);
|
|
1831
|
+
const result = buildProductionCss({ profile: options.profile });
|
|
1832
|
+
|
|
1833
|
+
console.log('\u2713 Generated production CSS: ' + path.relative(process.cwd(), result.outputPath));
|
|
1834
|
+
console.log('\u2713 File size: ' + (result.outputSize / 1024).toFixed(2) + ' KB');
|
|
1835
|
+
|
|
1836
|
+
if (!options.keepFull && fs.existsSync(fullCssPath)) {
|
|
1837
|
+
try {
|
|
1838
|
+
fs.unlinkSync(fullCssPath);
|
|
1839
|
+
console.log('Removed ' + path.relative(process.cwd(), fullCssPath) + ' for production build.');
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
// Windows FUSE: cannot always delete files cleanly, non-fatal.
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (options.profile) {
|
|
1846
|
+
const timings = result.profile || { purge: 0, minify: 0, write: 0 };
|
|
1847
|
+
const totalMs = Date.now() - totalStart;
|
|
1848
|
+
|
|
1849
|
+
console.log('\nEmilyCSS build profile\n');
|
|
1850
|
+
console.log('Framework: ' + frameworkMs + 'ms');
|
|
1851
|
+
console.log('Purge: ' + timings.purge + 'ms');
|
|
1852
|
+
console.log('Minify: ' + timings.minify + 'ms');
|
|
1853
|
+
console.log('Write: ' + timings.write + 'ms');
|
|
1854
|
+
console.log('Total: ' + totalMs + 'ms');
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
console.log('Build complete');
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (require.main === module) {
|
|
1861
|
+
const args = process.argv.slice(2);
|
|
1862
|
+
build({
|
|
1863
|
+
keepFull: args.includes('--keep-full'),
|
|
1864
|
+
profile: args.includes('--profile'),
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
module.exports = {
|
|
1869
|
+
build,
|
|
1870
|
+
buildFullFramework,
|
|
1871
|
+
buildProductionCss,
|
|
1872
|
+
ensureFullFramework,
|
|
1873
|
+
hexToOklch,
|
|
1874
|
+
oklchToHex,
|
|
1875
|
+
generateColourScale,
|
|
1876
|
+
generateAllColours,
|
|
1877
|
+
generateFontCSS,
|
|
1878
|
+
generateSpacing,
|
|
1879
|
+
generateBorderUtilities,
|
|
1880
|
+
generateColourUtilities,
|
|
1881
|
+
generateSemanticColourUtilities,
|
|
1882
|
+
generateTypographyUtilities,
|
|
1883
|
+
generateSpacingUtilities,
|
|
1884
|
+
generateFlexboxUtilities,
|
|
1885
|
+
generateGridUtilities,
|
|
1886
|
+
addStateVariants,
|
|
1887
|
+
addAriaDataVariants,
|
|
1888
|
+
addResponsiveVariants,
|
|
1889
|
+
generateManifest,
|
|
1890
|
+
};
|