@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.8.4",
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/schemas": "0.2.1",
55
- "@uniweb/runtime": "0.6.4"
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.4"
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
- console.warn(`Warning: Failed to render ${outputRoute}: ${err.message}`)
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
- 'secondary': 'var(--neutral-100)',
32
+ 'primary-border': 'transparent',
33
+ 'secondary': 'white',
33
34
  'secondary-foreground': 'var(--neutral-900)',
34
- 'secondary-hover': 'var(--neutral-200)',
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
- 'secondary': 'var(--neutral-200)',
60
+ 'primary-border': 'transparent',
61
+ 'secondary': 'white',
59
62
  'secondary-foreground': 'var(--neutral-900)',
60
- 'secondary-hover': 'var(--neutral-300)',
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',
@@ -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 contexts = {
426
- light: { ...defaultContexts.light, ...(rawConfig.contexts?.light || {}) },
427
- medium: { ...defaultContexts.medium, ...(rawConfig.contexts?.medium || {}) },
428
- dark: { ...defaultContexts.dark, ...(rawConfig.contexts?.dark || {}) },
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): Predictable lightness values, constant hue
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, // Base color - most vibrant
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=false] - If true, shade 500 will be the exact input color
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 fixed mode (predictable, constant hue)
455
+ * // Default: shade 500 = your exact color, shades redistributed
429
456
  * generateShades('#3b82f6')
430
457
  *
431
458
  * @example
432
- * // Natural mode (temperature-aware hue shifts)
433
- * generateShades('#3b82f6', { mode: 'natural' })
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 (higher saturation)
437
- * generateShades('#3b82f6', { mode: 'vivid', exactMatch: true })
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 = false } = options
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
- * Original fixed-lightness algorithm (default)
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
- // Handle exact match at 500
461
- if (exactMatch && level === 500) {
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
- const targetL = LIGHTNESS_MAP[level]
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 {