@uniweb/build 0.8.3 → 0.8.5

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.3",
3
+ "version": "0.8.5",
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.3"
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",
@@ -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 {