@uniweb/build 0.1.32 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/package.json +3 -3
- package/src/docs.js +2 -2
- package/src/generate-entry.js +26 -5
- package/src/index.js +2 -1
- package/src/prerender.js +18 -2
- package/src/schema.js +78 -11
- package/src/site/config.js +2 -1
- package/src/site/content-collector.js +265 -20
- package/src/site/plugin.js +14 -4
- package/src/theme/css-generator.js +341 -0
- package/src/theme/index.js +65 -0
- package/src/theme/processor.js +422 -0
- package/src/theme/shade-generator.js +666 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Processor
|
|
3
|
+
*
|
|
4
|
+
* Reads, validates, and processes theme configuration from theme.yml,
|
|
5
|
+
* merges with foundation defaults, and produces a complete theme config
|
|
6
|
+
* ready for CSS generation.
|
|
7
|
+
*
|
|
8
|
+
* @module @uniweb/build/theme/processor
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isValidColor, generatePalettes } from './shade-generator.js'
|
|
12
|
+
import { getDefaultColors, getDefaultContextTokens } from './css-generator.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default appearance configuration
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_APPEARANCE = {
|
|
18
|
+
default: 'light', // Default color scheme
|
|
19
|
+
allowToggle: false, // Whether to show scheme toggle
|
|
20
|
+
respectSystemPreference: true, // Honor prefers-color-scheme
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default font configuration
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_FONTS = {
|
|
27
|
+
body: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
28
|
+
heading: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
29
|
+
mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate color configuration
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} colors - Color configuration object
|
|
36
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
37
|
+
*/
|
|
38
|
+
function validateColors(colors) {
|
|
39
|
+
const errors = []
|
|
40
|
+
|
|
41
|
+
if (!colors || typeof colors !== 'object') {
|
|
42
|
+
return { valid: true, errors } // No colors is valid (use defaults)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const [name, value] of Object.entries(colors)) {
|
|
46
|
+
// Skip pre-defined palette objects
|
|
47
|
+
if (typeof value === 'object' && value !== null) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof value !== 'string') {
|
|
52
|
+
errors.push(`Color "${name}" must be a string or shade object, got ${typeof value}`)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isValidColor(value)) {
|
|
57
|
+
errors.push(`Color "${name}" has invalid value: ${value}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { valid: errors.length === 0, errors }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate context configuration
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} contexts - Context configuration object
|
|
68
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
69
|
+
*/
|
|
70
|
+
function validateContexts(contexts) {
|
|
71
|
+
const errors = []
|
|
72
|
+
|
|
73
|
+
if (!contexts || typeof contexts !== 'object') {
|
|
74
|
+
return { valid: true, errors }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const validContexts = ['light', 'medium', 'dark']
|
|
78
|
+
|
|
79
|
+
for (const [context, tokens] of Object.entries(contexts)) {
|
|
80
|
+
if (!validContexts.includes(context)) {
|
|
81
|
+
errors.push(`Unknown context "${context}". Valid contexts: ${validContexts.join(', ')}`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof tokens !== 'object' || tokens === null) {
|
|
86
|
+
errors.push(`Context "${context}" must be an object`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { valid: errors.length === 0, errors }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate font configuration
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} fonts - Font configuration object
|
|
97
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
98
|
+
*/
|
|
99
|
+
function validateFonts(fonts) {
|
|
100
|
+
const errors = []
|
|
101
|
+
|
|
102
|
+
if (!fonts || typeof fonts !== 'object') {
|
|
103
|
+
return { valid: true, errors }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate imports
|
|
107
|
+
if (fonts.import !== undefined) {
|
|
108
|
+
if (!Array.isArray(fonts.import)) {
|
|
109
|
+
errors.push('fonts.import must be an array')
|
|
110
|
+
} else {
|
|
111
|
+
for (const [index, item] of fonts.import.entries()) {
|
|
112
|
+
if (typeof item !== 'object' || !item.url) {
|
|
113
|
+
errors.push(`fonts.import[${index}] must have a "url" property`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { valid: errors.length === 0, errors }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate appearance configuration
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} appearance - Appearance configuration
|
|
126
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
127
|
+
*/
|
|
128
|
+
function validateAppearance(appearance) {
|
|
129
|
+
const errors = []
|
|
130
|
+
|
|
131
|
+
if (!appearance || typeof appearance !== 'object') {
|
|
132
|
+
// Simple string value (e.g., appearance: light)
|
|
133
|
+
if (typeof appearance === 'string') {
|
|
134
|
+
if (!['light', 'dark', 'system'].includes(appearance)) {
|
|
135
|
+
errors.push(`Invalid appearance value: ${appearance}. Must be "light", "dark", or "system"`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { valid: errors.length === 0, errors }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (appearance.default && !['light', 'dark', 'system'].includes(appearance.default)) {
|
|
142
|
+
errors.push(`Invalid appearance.default: ${appearance.default}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (appearance.schemes !== undefined) {
|
|
146
|
+
if (!Array.isArray(appearance.schemes)) {
|
|
147
|
+
errors.push('appearance.schemes must be an array')
|
|
148
|
+
} else {
|
|
149
|
+
const validSchemes = ['light', 'dark']
|
|
150
|
+
for (const scheme of appearance.schemes) {
|
|
151
|
+
if (!validSchemes.includes(scheme)) {
|
|
152
|
+
errors.push(`Invalid scheme: ${scheme}. Valid schemes: ${validSchemes.join(', ')}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { valid: errors.length === 0, errors }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate foundation variables configuration
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} vars - Foundation variables
|
|
165
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
166
|
+
*/
|
|
167
|
+
function validateFoundationVars(vars) {
|
|
168
|
+
const errors = []
|
|
169
|
+
|
|
170
|
+
if (!vars || typeof vars !== 'object') {
|
|
171
|
+
return { valid: true, errors }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const [name, config] of Object.entries(vars)) {
|
|
175
|
+
// Variable name validation
|
|
176
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(name)) {
|
|
177
|
+
errors.push(`Invalid variable name "${name}". Use lowercase letters, numbers, and hyphens.`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Config validation
|
|
181
|
+
if (typeof config !== 'object' && typeof config !== 'string' && typeof config !== 'number') {
|
|
182
|
+
errors.push(`Variable "${name}" must have a string, number, or config object value`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { valid: errors.length === 0, errors }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validate complete theme configuration
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} config - Raw theme configuration
|
|
193
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
194
|
+
*/
|
|
195
|
+
export function validateThemeConfig(config) {
|
|
196
|
+
const allErrors = []
|
|
197
|
+
|
|
198
|
+
if (!config || typeof config !== 'object') {
|
|
199
|
+
return { valid: true, errors: [] } // Empty config is valid (use all defaults)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const colorValidation = validateColors(config.colors)
|
|
203
|
+
const contextValidation = validateContexts(config.contexts)
|
|
204
|
+
const fontValidation = validateFonts(config.fonts)
|
|
205
|
+
const appearanceValidation = validateAppearance(config.appearance)
|
|
206
|
+
|
|
207
|
+
allErrors.push(...colorValidation.errors)
|
|
208
|
+
allErrors.push(...contextValidation.errors)
|
|
209
|
+
allErrors.push(...fontValidation.errors)
|
|
210
|
+
allErrors.push(...appearanceValidation.errors)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
valid: allErrors.length === 0,
|
|
214
|
+
errors: allErrors,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Normalize appearance configuration
|
|
220
|
+
*
|
|
221
|
+
* @param {string|Object} appearance - Raw appearance config
|
|
222
|
+
* @returns {Object} Normalized appearance config
|
|
223
|
+
*/
|
|
224
|
+
function normalizeAppearance(appearance) {
|
|
225
|
+
if (!appearance) {
|
|
226
|
+
return { ...DEFAULT_APPEARANCE }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Simple string value: "light", "dark", or "system"
|
|
230
|
+
if (typeof appearance === 'string') {
|
|
231
|
+
return {
|
|
232
|
+
default: appearance,
|
|
233
|
+
allowToggle: false,
|
|
234
|
+
respectSystemPreference: appearance === 'system',
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
...DEFAULT_APPEARANCE,
|
|
240
|
+
...appearance,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Merge foundation variables with site overrides
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} foundationVars - Variables from foundation vars.js
|
|
248
|
+
* @param {Object} siteVars - Site-level variable overrides
|
|
249
|
+
* @returns {Object} Merged variables
|
|
250
|
+
*/
|
|
251
|
+
function mergeFoundationVars(foundationVars = {}, siteVars = {}) {
|
|
252
|
+
const merged = {}
|
|
253
|
+
|
|
254
|
+
// Start with foundation defaults
|
|
255
|
+
for (const [name, config] of Object.entries(foundationVars)) {
|
|
256
|
+
merged[name] = typeof config === 'object' ? { ...config } : { default: config }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Apply site overrides
|
|
260
|
+
for (const [name, value] of Object.entries(siteVars)) {
|
|
261
|
+
if (merged[name]) {
|
|
262
|
+
// Override the default value
|
|
263
|
+
merged[name].default = value
|
|
264
|
+
} else {
|
|
265
|
+
// New variable from site
|
|
266
|
+
merged[name] = { default: value }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return merged
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Process raw theme configuration into a complete, validated config
|
|
275
|
+
*
|
|
276
|
+
* @param {Object} rawConfig - Raw theme.yml content
|
|
277
|
+
* @param {Object} options - Processing options
|
|
278
|
+
* @param {Object} options.foundationVars - Foundation variables from vars.js
|
|
279
|
+
* @param {boolean} options.strict - Throw on validation errors (default: false)
|
|
280
|
+
* @returns {{ config: Object, errors: string[], warnings: string[] }}
|
|
281
|
+
*/
|
|
282
|
+
export function processTheme(rawConfig = {}, options = {}) {
|
|
283
|
+
const { foundationVars = {}, strict = false } = options
|
|
284
|
+
const errors = []
|
|
285
|
+
const warnings = []
|
|
286
|
+
|
|
287
|
+
// Validate raw config
|
|
288
|
+
const validation = validateThemeConfig(rawConfig)
|
|
289
|
+
if (!validation.valid) {
|
|
290
|
+
errors.push(...validation.errors)
|
|
291
|
+
if (strict) {
|
|
292
|
+
throw new Error(`Theme configuration errors:\n${errors.join('\n')}`)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Process colors
|
|
297
|
+
const defaultColors = getDefaultColors()
|
|
298
|
+
const rawColors = rawConfig.colors || {}
|
|
299
|
+
|
|
300
|
+
// Filter to only valid colors (skip invalid ones in non-strict mode)
|
|
301
|
+
const validColors = {}
|
|
302
|
+
for (const [name, value] of Object.entries({ ...defaultColors, ...rawColors })) {
|
|
303
|
+
// Skip objects (pre-defined palettes) or invalid color strings
|
|
304
|
+
if (typeof value === 'object' && value !== null) {
|
|
305
|
+
validColors[name] = value
|
|
306
|
+
} else if (isValidColor(value)) {
|
|
307
|
+
validColors[name] = value
|
|
308
|
+
}
|
|
309
|
+
// Invalid colors are skipped (error already recorded during validation)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const colors = validColors
|
|
313
|
+
|
|
314
|
+
// Generate color palettes (shades 50-950 for each color)
|
|
315
|
+
// This is used by the Theme class for runtime color access
|
|
316
|
+
const palettes = generatePalettes(colors)
|
|
317
|
+
|
|
318
|
+
// Warn if required colors are missing
|
|
319
|
+
if (!rawConfig.colors?.primary) {
|
|
320
|
+
warnings.push('No primary color specified, using default blue (#3b82f6)')
|
|
321
|
+
}
|
|
322
|
+
if (!rawConfig.colors?.neutral) {
|
|
323
|
+
warnings.push('No neutral color specified, using default zinc (#71717a)')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Process contexts
|
|
327
|
+
const defaultContexts = getDefaultContextTokens()
|
|
328
|
+
const contexts = {
|
|
329
|
+
light: { ...defaultContexts.light, ...(rawConfig.contexts?.light || {}) },
|
|
330
|
+
medium: { ...defaultContexts.medium, ...(rawConfig.contexts?.medium || {}) },
|
|
331
|
+
dark: { ...defaultContexts.dark, ...(rawConfig.contexts?.dark || {}) },
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Process fonts
|
|
335
|
+
const fonts = {
|
|
336
|
+
...DEFAULT_FONTS,
|
|
337
|
+
...(rawConfig.fonts || {}),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Normalize and process appearance
|
|
341
|
+
const appearance = normalizeAppearance(rawConfig.appearance)
|
|
342
|
+
|
|
343
|
+
// Merge foundation variables with site overrides
|
|
344
|
+
const mergedFoundationVars = mergeFoundationVars(
|
|
345
|
+
foundationVars,
|
|
346
|
+
rawConfig.vars || rawConfig.foundationVars || {}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
// Validate merged foundation vars
|
|
350
|
+
const foundationValidation = validateFoundationVars(mergedFoundationVars)
|
|
351
|
+
if (!foundationValidation.valid) {
|
|
352
|
+
warnings.push(...foundationValidation.errors)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const config = {
|
|
356
|
+
colors, // Raw colors for CSS generator
|
|
357
|
+
palettes, // Generated palettes for Theme class
|
|
358
|
+
contexts,
|
|
359
|
+
fonts,
|
|
360
|
+
appearance,
|
|
361
|
+
foundationVars: mergedFoundationVars,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { config, errors, warnings }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Load foundation variables from vars.js export
|
|
369
|
+
*
|
|
370
|
+
* @param {Object} varsModule - Imported vars.js module
|
|
371
|
+
* @returns {Object} Foundation variables
|
|
372
|
+
*/
|
|
373
|
+
export function extractFoundationVars(varsModule) {
|
|
374
|
+
if (!varsModule) {
|
|
375
|
+
return {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle default export
|
|
379
|
+
const module = varsModule.default || varsModule
|
|
380
|
+
|
|
381
|
+
// Extract vars property or use whole object
|
|
382
|
+
return module.vars || module
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if a foundation has theme variables
|
|
387
|
+
*
|
|
388
|
+
* @param {Object} foundationSchema - Foundation schema.json content
|
|
389
|
+
* @returns {boolean}
|
|
390
|
+
*/
|
|
391
|
+
export function foundationHasVars(foundationSchema) {
|
|
392
|
+
// Check _self.vars (new), _self.themeVars (legacy), root themeVars (backwards compat)
|
|
393
|
+
return (
|
|
394
|
+
foundationSchema?._self?.vars != null ||
|
|
395
|
+
foundationSchema?._self?.themeVars != null ||
|
|
396
|
+
foundationSchema?.themeVars != null
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get foundation variables from schema
|
|
402
|
+
* Supports both new 'vars' and legacy 'themeVars' naming
|
|
403
|
+
*
|
|
404
|
+
* @param {Object} foundationSchema - Foundation schema.json content
|
|
405
|
+
* @returns {Object} Foundation variables
|
|
406
|
+
*/
|
|
407
|
+
export function getFoundationVars(foundationSchema) {
|
|
408
|
+
return (
|
|
409
|
+
foundationSchema?._self?.vars ||
|
|
410
|
+
foundationSchema?._self?.themeVars ||
|
|
411
|
+
foundationSchema?.themeVars ||
|
|
412
|
+
{}
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export default {
|
|
417
|
+
validateThemeConfig,
|
|
418
|
+
processTheme,
|
|
419
|
+
extractFoundationVars,
|
|
420
|
+
foundationHasVars,
|
|
421
|
+
getFoundationVars,
|
|
422
|
+
}
|