@tenphi/glaze 0.2.0 → 0.4.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 CHANGED
@@ -25,6 +25,7 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
25
25
  - **Light + Dark + High-Contrast** — all schemes from one definition
26
26
  - **Per-color hue override** — absolute or relative hue shifts within a theme
27
27
  - **Multi-format output** — `okhsl`, `rgb`, `hsl`, `oklch`
28
+ - **CSS custom properties export** — ready-to-use `--var: value;` declarations per scheme
28
29
  - **Import/Export** — serialize and restore theme configurations
29
30
  - **Create from hex/RGB** — start from an existing brand color
30
31
  - **Zero dependencies** — pure math, runs anywhere (Node.js, browser, edge)
@@ -69,7 +70,7 @@ const success = primary.extend({ hue: 157 });
69
70
  // Compose into a palette and export
70
71
  const palette = glaze.palette({ primary, danger, success });
71
72
  const tokens = palette.tokens({ prefix: true });
72
- // → { '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' }, ... }
73
+ // → { light: { 'primary-surface': 'okhsl(...)', ... }, dark: { 'primary-surface': 'okhsl(...)', ... } }
73
74
  ```
74
75
 
75
76
  ## Core Concepts
@@ -261,7 +262,8 @@ Create a single color token without a full theme:
261
262
  const accent = glaze.color({ hue: 280, saturation: 80, lightness: 52, mode: 'fixed' });
262
263
 
263
264
  accent.resolve(); // → ResolvedColor with light/dark/lightContrast/darkContrast
264
- accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
265
+ accent.token(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (tasty format)
266
+ accent.tasty(); // → { '': 'okhsl(...)', '@dark': 'okhsl(...)' } (same as token)
265
267
  accent.json(); // → { light: 'okhsl(...)', dark: 'okhsl(...)' }
266
268
  ```
267
269
 
@@ -306,7 +308,7 @@ theme.tokens({ format: 'hsl' }); // → 'hsl(270.5, 45.2%, 95.8%)'
306
308
  theme.tokens({ format: 'oklch' }); // → 'oklch(96.5% 0.0123 280.0)'
307
309
  ```
308
310
 
309
- The `format` option works on all export methods: `theme.tokens()`, `theme.json()`, `palette.tokens()`, `palette.json()`, and standalone `glaze.color().token()` / `.json()`.
311
+ The `format` option works on all export methods: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `palette.tokens()`, `palette.tasty()`, `palette.json()`, `palette.css()`, and standalone `glaze.color().token()` / `.tasty()` / `.json()`.
310
312
 
311
313
  Available formats:
312
314
 
@@ -409,18 +411,93 @@ const palette = glaze.palette({ primary, danger, success, warning });
409
411
 
410
412
  ### Token Export
411
413
 
414
+ Tokens are grouped by scheme variant, with plain color names as keys:
415
+
412
416
  ```ts
413
417
  const tokens = palette.tokens({ prefix: true });
414
418
  // → {
419
+ // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
420
+ // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
421
+ // }
422
+ ```
423
+
424
+ Custom prefix mapping:
425
+
426
+ ```ts
427
+ palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
428
+ ```
429
+
430
+ ### Tasty Export (for [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style system)
431
+
432
+ The `tasty()` method exports tokens in the [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state binding format — `#name` color token keys with state aliases (`''`, `@dark`, etc.):
433
+
434
+ ```ts
435
+ const tastyTokens = palette.tasty({ prefix: true });
436
+ // → {
415
437
  // '#primary-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
416
438
  // '#danger-surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
417
439
  // }
418
440
  ```
419
441
 
442
+ Apply as global styles to make color tokens available app-wide:
443
+
444
+ ```ts
445
+ import { useGlobalStyles } from '@cube-dev/ui-kit';
446
+
447
+ // In your root component
448
+ useGlobalStyles('body', tastyTokens);
449
+ ```
450
+
451
+ For zero-runtime builds, use `tastyStatic` to generate the CSS at build time:
452
+
453
+ ```ts
454
+ import { tastyStatic } from '@cube-dev/ui-kit';
455
+
456
+ tastyStatic('body', tastyTokens);
457
+ ```
458
+
459
+ Alternatively, register as a recipe via `configure()`:
460
+
461
+ ```ts
462
+ import { configure, tasty } from '@cube-dev/ui-kit';
463
+
464
+ configure({
465
+ recipes: {
466
+ 'all-themes': tastyTokens,
467
+ },
468
+ });
469
+
470
+ const Page = tasty({
471
+ styles: {
472
+ recipe: 'all-themes',
473
+ fill: '#primary-surface',
474
+ color: '#primary-text',
475
+ },
476
+ });
477
+ ```
478
+
479
+ Or spread directly into component styles:
480
+
481
+ ```ts
482
+ const Card = tasty({
483
+ styles: {
484
+ ...tastyTokens,
485
+ fill: '#primary-surface',
486
+ color: '#primary-text',
487
+ },
488
+ });
489
+ ```
490
+
420
491
  Custom prefix mapping:
421
492
 
422
493
  ```ts
423
- palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
494
+ palette.tasty({ prefix: { primary: 'brand-', danger: 'error-' } });
495
+ ```
496
+
497
+ Custom state aliases:
498
+
499
+ ```ts
500
+ palette.tasty({ states: { dark: '@dark', highContrast: '@hc' } });
424
501
  ```
425
502
 
426
503
  ### JSON Export (Framework-Agnostic)
@@ -433,6 +510,53 @@ const data = palette.json({ prefix: true });
433
510
  // }
434
511
  ```
435
512
 
513
+ ### CSS Export
514
+
515
+ Export as CSS custom property declarations, grouped by scheme variant. Each variant is a string of `--name-color: value;` lines that you can wrap in your own selectors and media queries.
516
+
517
+ ```ts
518
+ const css = theme.css();
519
+ // css.light → "--surface-color: rgb(...);\n--text-color: rgb(...);"
520
+ // css.dark → "--surface-color: rgb(...);\n--text-color: rgb(...);"
521
+ // css.lightContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
522
+ // css.darkContrast → "--surface-color: rgb(...);\n--text-color: rgb(...);"
523
+ ```
524
+
525
+ Use in a stylesheet:
526
+
527
+ ```ts
528
+ const css = palette.css({ prefix: true });
529
+
530
+ const stylesheet = `
531
+ :root { ${css.light} }
532
+ @media (prefers-color-scheme: dark) {
533
+ :root { ${css.dark} }
534
+ }
535
+ `;
536
+ ```
537
+
538
+ Options:
539
+
540
+ | Option | Default | Description |
541
+ |---|---|---|
542
+ | `format` | `'rgb'` | Color format (`'rgb'`, `'hsl'`, `'okhsl'`, `'oklch'`) |
543
+ | `suffix` | `'-color'` | Suffix appended to each CSS property name |
544
+ | `prefix` | — | (palette only) Same prefix behavior as `tokens()` |
545
+
546
+ ```ts
547
+ // Custom suffix
548
+ theme.css({ suffix: '' });
549
+ // → "--surface: rgb(...);"
550
+
551
+ // Custom format
552
+ theme.css({ format: 'hsl' });
553
+ // → "--surface-color: hsl(...);"
554
+
555
+ // Palette with prefix
556
+ palette.css({ prefix: true });
557
+ // → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
558
+ ```
559
+
436
560
  ## Output Modes
437
561
 
438
562
  Control which scheme variants appear in exports:
@@ -440,17 +564,22 @@ Control which scheme variants appear in exports:
440
564
  ```ts
441
565
  // Light only
442
566
  palette.tokens({ modes: { dark: false, highContrast: false } });
567
+ // → { light: { ... } }
443
568
 
444
569
  // Light + dark (default)
445
570
  palette.tokens({ modes: { highContrast: false } });
571
+ // → { light: { ... }, dark: { ... } }
446
572
 
447
573
  // All four variants
448
574
  palette.tokens({ modes: { dark: true, highContrast: true } });
575
+ // → { light: { ... }, dark: { ... }, lightContrast: { ... }, darkContrast: { ... } }
449
576
  ```
450
577
 
578
+ The `modes` option works the same way on `tokens()`, `tasty()`, `json()`, and `css()`.
579
+
451
580
  Resolution priority (highest first):
452
581
 
453
- 1. `tokens({ modes })` / `json({ modes })` — per-call override
582
+ 1. `tokens({ modes })` / `tasty({ modes })` / `json({ modes })` / `css({ ... })` — per-call override
454
583
  2. `glaze.configure({ modes })` — global config
455
584
  3. Built-in default: `{ dark: true, highContrast: false }`
456
585
 
@@ -561,12 +690,24 @@ const note = primary.extend({ hue: 302 });
561
690
 
562
691
  const palette = glaze.palette({ primary, danger, success, warning, note });
563
692
 
564
- // Export as OKHSL tokens (default)
693
+ // Export as flat token map grouped by variant
565
694
  const tokens = palette.tokens({ prefix: true });
695
+ // tokens.light → { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' }
696
+ // tokens.dark → { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' }
697
+
698
+ // Export as tasty style-to-state bindings (for Tasty style system)
699
+ const tastyTokens = palette.tasty({ prefix: true });
700
+ // tastyTokens['#primary-surface'] → { '': 'okhsl(...)', '@dark': 'okhsl(...)' }
701
+ // Use as a recipe or spread into component styles (see Tasty Export section)
566
702
 
567
703
  // Export as RGB for broader CSS compatibility
568
704
  const rgbTokens = palette.tokens({ prefix: true, format: 'rgb' });
569
705
 
706
+ // Export as CSS custom properties (rgb format by default)
707
+ const css = palette.css({ prefix: true });
708
+ // css.light → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
709
+ // css.dark → "--primary-surface-color: rgb(...);\n--danger-surface-color: rgb(...);"
710
+
570
711
  // Save and restore a theme
571
712
  const snapshot = primary.export();
572
713
  const restored = glaze.from(snapshot);
@@ -603,8 +744,10 @@ brand.colors({ surface: { lightness: 97 }, text: { base: 'surface', lightness: '
603
744
  | `theme.export()` | Export configuration as JSON-safe object |
604
745
  | `theme.extend(options)` | Create a child theme |
605
746
  | `theme.resolve()` | Resolve all colors |
606
- | `theme.tokens(options?)` | Export as token map |
747
+ | `theme.tokens(options?)` | Export as flat token map grouped by variant |
748
+ | `theme.tasty(options?)` | Export as [Tasty](https://cube-ui-kit.vercel.app/?path=/docs/tasty-documentation--docs) style-to-state bindings |
607
749
  | `theme.json(options?)` | Export as plain JSON |
750
+ | `theme.css(options?)` | Export as CSS custom property declarations |
608
751
 
609
752
  ### Global Configuration
610
753
 
package/dist/index.cjs CHANGED
@@ -998,6 +998,20 @@ function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
998
998
  }
999
999
  return tokens;
1000
1000
  }
1001
+ function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl") {
1002
+ const result = { light: {} };
1003
+ if (modes.dark) result.dark = {};
1004
+ if (modes.highContrast) result.lightContrast = {};
1005
+ if (modes.dark && modes.highContrast) result.darkContrast = {};
1006
+ for (const [name, color] of resolved) {
1007
+ const key = `${prefix}${name}`;
1008
+ result.light[key] = formatVariant(color.light, format);
1009
+ if (modes.dark) result.dark[key] = formatVariant(color.dark, format);
1010
+ if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format);
1011
+ if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format);
1012
+ }
1013
+ return result;
1014
+ }
1001
1015
  function buildJsonMap(resolved, modes, format = "okhsl") {
1002
1016
  const result = {};
1003
1017
  for (const [name, color] of resolved) {
@@ -1009,6 +1023,27 @@ function buildJsonMap(resolved, modes, format = "okhsl") {
1009
1023
  }
1010
1024
  return result;
1011
1025
  }
1026
+ function buildCssMap(resolved, prefix, suffix, format) {
1027
+ const lines = {
1028
+ light: [],
1029
+ dark: [],
1030
+ lightContrast: [],
1031
+ darkContrast: []
1032
+ };
1033
+ for (const [name, color] of resolved) {
1034
+ const prop = `--${prefix}${name}${suffix}`;
1035
+ lines.light.push(`${prop}: ${formatVariant(color.light, format)};`);
1036
+ lines.dark.push(`${prop}: ${formatVariant(color.dark, format)};`);
1037
+ lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format)};`);
1038
+ lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format)};`);
1039
+ }
1040
+ return {
1041
+ light: lines.light.join("\n"),
1042
+ dark: lines.dark.join("\n"),
1043
+ lightContrast: lines.lightContrast.join("\n"),
1044
+ darkContrast: lines.darkContrast.join("\n")
1045
+ };
1046
+ }
1012
1047
  function createTheme(hue, saturation, initialColors) {
1013
1048
  let colorDefs = initialColors ? { ...initialColors } : {};
1014
1049
  return {
@@ -1058,6 +1093,9 @@ function createTheme(hue, saturation, initialColors) {
1058
1093
  return resolveAllColors(hue, saturation, colorDefs);
1059
1094
  },
1060
1095
  tokens(options) {
1096
+ return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
1097
+ },
1098
+ tasty(options) {
1061
1099
  return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
1062
1100
  dark: options?.states?.dark ?? globalConfig.states.dark,
1063
1101
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1065,12 +1103,32 @@ function createTheme(hue, saturation, initialColors) {
1065
1103
  },
1066
1104
  json(options) {
1067
1105
  return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
1106
+ },
1107
+ css(options) {
1108
+ return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
1068
1109
  }
1069
1110
  };
1070
1111
  }
1112
+ function resolvePrefix(options, themeName) {
1113
+ if (options?.prefix === true) return `${themeName}-`;
1114
+ if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1115
+ return "";
1116
+ }
1071
1117
  function createPalette(themes) {
1072
1118
  return {
1073
1119
  tokens(options) {
1120
+ const modes = resolveModes(options?.modes);
1121
+ const allTokens = {};
1122
+ for (const [themeName, theme] of Object.entries(themes)) {
1123
+ const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1124
+ for (const variant of Object.keys(tokens)) {
1125
+ if (!allTokens[variant]) allTokens[variant] = {};
1126
+ Object.assign(allTokens[variant], tokens[variant]);
1127
+ }
1128
+ }
1129
+ return allTokens;
1130
+ },
1131
+ tasty(options) {
1074
1132
  const states = {
1075
1133
  dark: options?.states?.dark ?? globalConfig.states.dark,
1076
1134
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1078,11 +1136,7 @@ function createPalette(themes) {
1078
1136
  const modes = resolveModes(options?.modes);
1079
1137
  const allTokens = {};
1080
1138
  for (const [themeName, theme] of Object.entries(themes)) {
1081
- const resolved = theme.resolve();
1082
- let prefix = "";
1083
- if (options?.prefix === true) prefix = `${themeName}-`;
1084
- else if (typeof options?.prefix === "object" && options.prefix !== null) prefix = options.prefix[themeName] ?? `${themeName}-`;
1085
- const tokens = buildTokenMap(resolved, prefix, states, modes, options?.format);
1139
+ const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1086
1140
  Object.assign(allTokens, tokens);
1087
1141
  }
1088
1142
  return allTokens;
@@ -1092,6 +1146,31 @@ function createPalette(themes) {
1092
1146
  const result = {};
1093
1147
  for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
1094
1148
  return result;
1149
+ },
1150
+ css(options) {
1151
+ const suffix = options?.suffix ?? "-color";
1152
+ const format = options?.format ?? "rgb";
1153
+ const allLines = {
1154
+ light: [],
1155
+ dark: [],
1156
+ lightContrast: [],
1157
+ darkContrast: []
1158
+ };
1159
+ for (const [themeName, theme] of Object.entries(themes)) {
1160
+ const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1161
+ for (const key of [
1162
+ "light",
1163
+ "dark",
1164
+ "lightContrast",
1165
+ "darkContrast"
1166
+ ]) if (css[key]) allLines[key].push(css[key]);
1167
+ }
1168
+ return {
1169
+ light: allLines.light.join("\n"),
1170
+ dark: allLines.dark.join("\n"),
1171
+ lightContrast: allLines.lightContrast.join("\n"),
1172
+ darkContrast: allLines.darkContrast.join("\n")
1173
+ };
1095
1174
  }
1096
1175
  };
1097
1176
  }
@@ -1111,6 +1190,12 @@ function createColorToken(input) {
1111
1190
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1112
1191
  }, resolveModes(options?.modes), options?.format)["#__color__"];
1113
1192
  },
1193
+ tasty(options) {
1194
+ return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
1195
+ dark: options?.states?.dark ?? globalConfig.states.dark,
1196
+ highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
1197
+ }, resolveModes(options?.modes), options?.format)["#__color__"];
1198
+ },
1114
1199
  json(options) {
1115
1200
  return buildJsonMap(resolveAllColors(input.hue, input.saturation, defs), resolveModes(options?.modes), options?.format)["__color__"];
1116
1201
  }