@uniweb/build 0.8.4 → 0.8.6
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/package.json +4 -4
- package/src/prerender.js +18 -1
- package/src/theme/css-generator.js +12 -4
- package/src/theme/processor.js +52 -5
- package/src/theme/shade-generator.js +61 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"@uniweb/content-reader": "1.1.4",
|
|
54
|
-
"@uniweb/
|
|
55
|
-
"@uniweb/
|
|
54
|
+
"@uniweb/runtime": "0.6.4",
|
|
55
|
+
"@uniweb/schemas": "0.2.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@tailwindcss/vite": "^4.0.0",
|
|
62
62
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
63
63
|
"vite-plugin-svgr": "^4.0.0",
|
|
64
|
-
"@uniweb/core": "0.5.
|
|
64
|
+
"@uniweb/core": "0.5.5"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"vite": {
|
package/src/prerender.js
CHANGED
|
@@ -825,7 +825,24 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
825
825
|
try {
|
|
826
826
|
renderedContent = renderToString(element)
|
|
827
827
|
} catch (err) {
|
|
828
|
-
|
|
828
|
+
const msg = err.message || ''
|
|
829
|
+
|
|
830
|
+
if (msg.includes('Invalid hook call') || msg.includes('useState') || msg.includes('useEffect')) {
|
|
831
|
+
console.warn(
|
|
832
|
+
` Skipped SSG for ${outputRoute} — contains components with React hooks ` +
|
|
833
|
+
`(useState/useEffect) that cannot render during pre-rendering. ` +
|
|
834
|
+
`The page will render correctly client-side.`
|
|
835
|
+
)
|
|
836
|
+
} else if (msg.includes('Element type is invalid') && msg.includes('null')) {
|
|
837
|
+
console.warn(
|
|
838
|
+
` Skipped SSG for ${outputRoute} — a component resolved to null during pre-rendering. ` +
|
|
839
|
+
`This often happens with components that use React hooks. ` +
|
|
840
|
+
`The page will render correctly client-side.`
|
|
841
|
+
)
|
|
842
|
+
} else {
|
|
843
|
+
console.warn(` Warning: Failed to render ${outputRoute}: ${msg}`)
|
|
844
|
+
}
|
|
845
|
+
|
|
829
846
|
if (process.env.DEBUG) {
|
|
830
847
|
console.error(err.stack)
|
|
831
848
|
}
|
|
@@ -29,9 +29,11 @@ const DEFAULT_CONTEXT_TOKENS = {
|
|
|
29
29
|
'primary': 'var(--primary-600)',
|
|
30
30
|
'primary-foreground': 'white',
|
|
31
31
|
'primary-hover': 'var(--primary-700)',
|
|
32
|
-
'
|
|
32
|
+
'primary-border': 'transparent',
|
|
33
|
+
'secondary': 'white',
|
|
33
34
|
'secondary-foreground': 'var(--neutral-900)',
|
|
34
|
-
'secondary-hover': 'var(--neutral-
|
|
35
|
+
'secondary-hover': 'var(--neutral-100)',
|
|
36
|
+
'secondary-border': 'var(--neutral-300)',
|
|
35
37
|
'success': '#16a34a',
|
|
36
38
|
'success-subtle': '#f0fdf4',
|
|
37
39
|
'warning': '#d97706',
|
|
@@ -55,9 +57,11 @@ const DEFAULT_CONTEXT_TOKENS = {
|
|
|
55
57
|
'primary': 'var(--primary-600)',
|
|
56
58
|
'primary-foreground': 'white',
|
|
57
59
|
'primary-hover': 'var(--primary-700)',
|
|
58
|
-
'
|
|
60
|
+
'primary-border': 'transparent',
|
|
61
|
+
'secondary': 'white',
|
|
59
62
|
'secondary-foreground': 'var(--neutral-900)',
|
|
60
|
-
'secondary-hover': 'var(--neutral-
|
|
63
|
+
'secondary-hover': 'var(--neutral-100)',
|
|
64
|
+
'secondary-border': 'var(--neutral-300)',
|
|
61
65
|
'success': '#16a34a',
|
|
62
66
|
'success-subtle': '#f0fdf4',
|
|
63
67
|
'warning': '#d97706',
|
|
@@ -81,9 +85,11 @@ const DEFAULT_CONTEXT_TOKENS = {
|
|
|
81
85
|
'primary': 'var(--primary-500)',
|
|
82
86
|
'primary-foreground': 'white',
|
|
83
87
|
'primary-hover': 'var(--primary-400)',
|
|
88
|
+
'primary-border': 'transparent',
|
|
84
89
|
'secondary': 'var(--neutral-800)',
|
|
85
90
|
'secondary-foreground': 'var(--neutral-100)',
|
|
86
91
|
'secondary-hover': 'var(--neutral-700)',
|
|
92
|
+
'secondary-border': 'var(--neutral-600)',
|
|
87
93
|
'success': '#4ade80',
|
|
88
94
|
'success-subtle': '#052e16',
|
|
89
95
|
'warning': '#fbbf24',
|
|
@@ -179,9 +185,11 @@ function generateDarkSchemeCSS(config = {}) {
|
|
|
179
185
|
'primary': 'var(--primary-500)',
|
|
180
186
|
'primary-foreground': 'white',
|
|
181
187
|
'primary-hover': 'var(--primary-400)',
|
|
188
|
+
'primary-border': 'transparent',
|
|
182
189
|
'secondary': 'var(--neutral-800)',
|
|
183
190
|
'secondary-foreground': 'var(--neutral-100)',
|
|
184
191
|
'secondary-hover': 'var(--neutral-700)',
|
|
192
|
+
'secondary-border': 'var(--neutral-600)',
|
|
185
193
|
'success': '#4ade80',
|
|
186
194
|
'success-subtle': '#052e16',
|
|
187
195
|
'warning': '#fbbf24',
|
package/src/theme/processor.js
CHANGED
|
@@ -85,6 +85,45 @@ const DEFAULT_CODE_THEME = {
|
|
|
85
85
|
selection: '#45475a', // Selection background
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Valid shade levels for palette references
|
|
90
|
+
*/
|
|
91
|
+
const SHADE_LEVELS = new Set([50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950])
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve context token values to valid CSS.
|
|
95
|
+
*
|
|
96
|
+
* Content authors write palette references as bare names: `primary-500`,
|
|
97
|
+
* `neutral-200`. This is the natural syntax in theme.yml. The processor
|
|
98
|
+
* resolves these to `var(--primary-500)` etc. Plain CSS values (hex, var(),
|
|
99
|
+
* named colors) pass through as-is — that's the escape hatch.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} value - The token value from theme.yml
|
|
102
|
+
* @returns {string} Valid CSS value
|
|
103
|
+
*/
|
|
104
|
+
function normalizePaletteRef(value) {
|
|
105
|
+
if (typeof value !== 'string') return value
|
|
106
|
+
|
|
107
|
+
// Already a CSS function (var(), rgb(), etc.) — pass through
|
|
108
|
+
if (value.includes('(')) return value
|
|
109
|
+
|
|
110
|
+
// Hex color — pass through
|
|
111
|
+
if (value.startsWith('#')) return value
|
|
112
|
+
|
|
113
|
+
// Bare palette reference: "primary-500", "--primary-500"
|
|
114
|
+
const bare = value.replace(/^-{0,2}/, '')
|
|
115
|
+
const match = bare.match(/^([a-z][a-z0-9]*)-(\d+)$/)
|
|
116
|
+
|
|
117
|
+
if (match) {
|
|
118
|
+
const shade = parseInt(match[2], 10)
|
|
119
|
+
if (SHADE_LEVELS.has(shade)) {
|
|
120
|
+
return `var(--${bare})`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return value
|
|
125
|
+
}
|
|
126
|
+
|
|
88
127
|
/**
|
|
89
128
|
* Validate color configuration
|
|
90
129
|
*
|
|
@@ -420,12 +459,20 @@ export function processTheme(rawConfig = {}, options = {}) {
|
|
|
420
459
|
warnings.push('No neutral color specified, using default stone (#78716c)')
|
|
421
460
|
}
|
|
422
461
|
|
|
423
|
-
// Process contexts
|
|
462
|
+
// Process contexts (resolve bare palette refs like "primary-500" to var())
|
|
424
463
|
const defaultContexts = getDefaultContextTokens()
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
464
|
+
const rawContexts = rawConfig.contexts || {}
|
|
465
|
+
const contexts = {}
|
|
466
|
+
|
|
467
|
+
for (const name of ['light', 'medium', 'dark']) {
|
|
468
|
+
const overrides = rawContexts[name] || {}
|
|
469
|
+
const normalized = {}
|
|
470
|
+
|
|
471
|
+
for (const [token, value] of Object.entries(overrides)) {
|
|
472
|
+
normalized[token] = normalizePaletteRef(value)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
contexts[name] = { ...defaultContexts[name], ...normalized }
|
|
429
476
|
}
|
|
430
477
|
|
|
431
478
|
// Process fonts
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* Generates 11 color shades (50-950) from a single base color using
|
|
5
5
|
* the OKLCH color space for perceptually uniform results.
|
|
6
6
|
*
|
|
7
|
+
* By default, shade 500 preserves the exact input color and surrounding
|
|
8
|
+
* shades are redistributed proportionally to maintain a monotonic lightness
|
|
9
|
+
* scale. This means `primary: "#E35D25"` guarantees that exact color appears
|
|
10
|
+
* at shade 500, regardless of its natural lightness. Set `exactMatch: false`
|
|
11
|
+
* to use fixed lightness values instead (shade 500 forced to lightness 0.55).
|
|
12
|
+
*
|
|
7
13
|
* Supports multiple generation modes:
|
|
8
|
-
* - 'fixed' (default):
|
|
14
|
+
* - 'fixed' (default): Constant hue, proportional lightness redistribution
|
|
9
15
|
* - 'natural': Temperature-aware hue shifts, curved chroma
|
|
10
16
|
* - 'vivid': Higher saturation, more dramatic chroma curve
|
|
11
17
|
*
|
|
@@ -23,7 +29,7 @@ const LIGHTNESS_MAP = {
|
|
|
23
29
|
200: 0.87,
|
|
24
30
|
300: 0.78,
|
|
25
31
|
400: 0.68,
|
|
26
|
-
500: 0.55, //
|
|
32
|
+
500: 0.55, // Reference midpoint (smart mode uses actual input lightness)
|
|
27
33
|
600: 0.48,
|
|
28
34
|
700: 0.40,
|
|
29
35
|
800: 0.32,
|
|
@@ -47,6 +53,20 @@ const CHROMA_SCALE = {
|
|
|
47
53
|
950: 0.45, // Reduced chroma at dark end
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
// Relative positions of each shade within the light/dark halves of the LIGHTNESS_MAP.
|
|
57
|
+
// Used by smart matching to redistribute shades proportionally around the input color
|
|
58
|
+
// while preserving the perceptual spacing of the original map.
|
|
59
|
+
const LIGHT_HALF_RANGE = LIGHTNESS_MAP[50] - LIGHTNESS_MAP[500]
|
|
60
|
+
const DARK_HALF_RANGE = LIGHTNESS_MAP[500] - LIGHTNESS_MAP[950]
|
|
61
|
+
const RELATIVE_POSITION = {}
|
|
62
|
+
for (const level of SHADE_LEVELS) {
|
|
63
|
+
if (level < 500) {
|
|
64
|
+
RELATIVE_POSITION[level] = (LIGHTNESS_MAP[level] - LIGHTNESS_MAP[500]) / LIGHT_HALF_RANGE
|
|
65
|
+
} else if (level > 500) {
|
|
66
|
+
RELATIVE_POSITION[level] = (LIGHTNESS_MAP[500] - LIGHTNESS_MAP[level]) / DARK_HALF_RANGE
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
// Mode-specific configurations
|
|
51
71
|
const MODE_CONFIG = {
|
|
52
72
|
// Fixed mode: predictable, consistent (current default behavior)
|
|
@@ -417,27 +437,34 @@ export function formatHex(r, g, b) {
|
|
|
417
437
|
/**
|
|
418
438
|
* Generate color shades from a base color
|
|
419
439
|
*
|
|
440
|
+
* By default, shade 500 preserves the exact input color and surrounding shades
|
|
441
|
+
* are redistributed proportionally to maintain a monotonic lightness scale.
|
|
442
|
+
* Set `exactMatch: false` to use fixed lightness values instead (shade 500
|
|
443
|
+
* may differ from your input).
|
|
444
|
+
*
|
|
420
445
|
* @param {string} color - Base color in any supported format
|
|
421
446
|
* @param {Object} options - Options
|
|
422
447
|
* @param {string} [options.format='oklch'] - Output format: 'oklch' or 'hex'
|
|
423
448
|
* @param {string} [options.mode='fixed'] - Generation mode: 'fixed', 'natural', or 'vivid'
|
|
424
|
-
* @param {boolean} [options.exactMatch
|
|
449
|
+
* @param {boolean} [options.exactMatch] - Controls shade 500 matching. Default (undefined/true):
|
|
450
|
+
* shade 500 = exact input, other shades redistributed proportionally. False: all shades use
|
|
451
|
+
* fixed lightness values (shade 500 may not match input).
|
|
425
452
|
* @returns {Object} Object with shade levels as keys (50-950) and color values
|
|
426
453
|
*
|
|
427
454
|
* @example
|
|
428
|
-
* // Default
|
|
455
|
+
* // Default: shade 500 = your exact color, shades redistributed
|
|
429
456
|
* generateShades('#3b82f6')
|
|
430
457
|
*
|
|
431
458
|
* @example
|
|
432
|
-
* //
|
|
433
|
-
* generateShades('#3b82f6', {
|
|
459
|
+
* // Opt out: use fixed lightness scale (shade 500 may differ from input)
|
|
460
|
+
* generateShades('#3b82f6', { exactMatch: false })
|
|
434
461
|
*
|
|
435
462
|
* @example
|
|
436
|
-
* // Vivid mode
|
|
437
|
-
* generateShades('#3b82f6', { mode: 'vivid'
|
|
463
|
+
* // Vivid mode with default smart matching
|
|
464
|
+
* generateShades('#3b82f6', { mode: 'vivid' })
|
|
438
465
|
*/
|
|
439
466
|
export function generateShades(color, options = {}) {
|
|
440
|
-
const { format = 'oklch', mode = 'fixed', exactMatch
|
|
467
|
+
const { format = 'oklch', mode = 'fixed', exactMatch } = options
|
|
441
468
|
const base = parseColor(color)
|
|
442
469
|
const config = MODE_CONFIG[mode] || MODE_CONFIG.fixed
|
|
443
470
|
|
|
@@ -451,14 +478,21 @@ export function generateShades(color, options = {}) {
|
|
|
451
478
|
}
|
|
452
479
|
|
|
453
480
|
/**
|
|
454
|
-
*
|
|
481
|
+
* Fixed-hue algorithm with smart lightness redistribution.
|
|
482
|
+
*
|
|
483
|
+
* Default (exactMatch !== false): shade 500 = exact input color, other shades
|
|
484
|
+
* redistributed proportionally around the input's actual lightness. This preserves
|
|
485
|
+
* the perceptual spacing of the LIGHTNESS_MAP while guaranteeing a monotonic scale.
|
|
486
|
+
*
|
|
487
|
+
* exactMatch === false: all shades use fixed LIGHTNESS_MAP values (original behavior).
|
|
455
488
|
*/
|
|
456
489
|
function generateFixedShades(base, originalColor, format, exactMatch) {
|
|
457
490
|
const shades = {}
|
|
491
|
+
const smart = exactMatch !== false
|
|
458
492
|
|
|
459
493
|
for (const level of SHADE_LEVELS) {
|
|
460
|
-
//
|
|
461
|
-
if (
|
|
494
|
+
// Shade 500: use exact input color in smart mode
|
|
495
|
+
if (smart && level === 500) {
|
|
462
496
|
if (format === 'hex') {
|
|
463
497
|
shades[level] = originalColor.startsWith('#') ? originalColor : formatHexFromOklch(base)
|
|
464
498
|
} else {
|
|
@@ -467,7 +501,19 @@ function generateFixedShades(base, originalColor, format, exactMatch) {
|
|
|
467
501
|
continue
|
|
468
502
|
}
|
|
469
503
|
|
|
470
|
-
|
|
504
|
+
// Compute target lightness
|
|
505
|
+
let targetL
|
|
506
|
+
if (smart) {
|
|
507
|
+
// Redistribute proportionally around input lightness
|
|
508
|
+
if (level < 500) {
|
|
509
|
+
targetL = base.l + RELATIVE_POSITION[level] * (LIGHTNESS_MAP[50] - base.l)
|
|
510
|
+
} else {
|
|
511
|
+
targetL = base.l - RELATIVE_POSITION[level] * (base.l - LIGHTNESS_MAP[950])
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
targetL = LIGHTNESS_MAP[level]
|
|
515
|
+
}
|
|
516
|
+
|
|
471
517
|
const chromaScale = CHROMA_SCALE[level]
|
|
472
518
|
const targetC = base.c * chromaScale
|
|
473
519
|
|
|
@@ -515,8 +561,8 @@ function generateEnhancedShades(base, originalColor, format, config, exactMatch)
|
|
|
515
561
|
for (let i = 0; i < SHADE_LEVELS.length; i++) {
|
|
516
562
|
const level = SHADE_LEVELS[i]
|
|
517
563
|
|
|
518
|
-
// Handle exact match at 500 (index 5)
|
|
519
|
-
if (exactMatch && level === 500) {
|
|
564
|
+
// Handle exact match at 500 (index 5) — default behavior
|
|
565
|
+
if (exactMatch !== false && level === 500) {
|
|
520
566
|
if (format === 'hex') {
|
|
521
567
|
shades[level] = originalColor.startsWith('#') ? originalColor : formatHexFromOklch(base)
|
|
522
568
|
} else {
|