@tenphi/glaze 1.3.0 → 1.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/docs/api.md CHANGED
@@ -559,8 +559,9 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
559
559
 
560
560
  | Field | Type | Description |
561
561
  | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
562
- | `tone` | `HCPair<ToneValue>` | Number = absolute (0–100). `'+N'`/`'-N'` = a signed **tone delta** from the base (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. |
563
- | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
562
+ | `from` | `GlazeColorValue` | Seed this color from a literal value (hex, `rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`, or a value object). Supplies `hue`, `tone`, and an **absolute** saturation — the one way a theme color escapes the seed ceiling. Light/normal-contrast reproduces it exactly. See [`from`](#from-a-literal-color). |
563
+ | `tone` | `HCPair<ToneValue>` | Number = absolute (0–100). `'+N'`/`'-N'` = a signed **tone delta** from the base (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. Defaults to the tone of `from`. |
564
+ | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. With `from`, setting it overrides the color's absolute saturation and reverts to a factor of the seed. |
564
565
  | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
565
566
  | `darkHue` | `number \| RelativeValue` | Dark-scheme hue. Number = absolute (0–360). String = relative to the **theme dark seed hue**. Falls back to `hue`. See [Dark seed](#dark-seed-darkhue--darksaturation). |
566
567
  | `darkSaturation` | `number` | Dark-scheme saturation factor (0–1) over the dark seed saturation. Falls back to `saturation`. When set, the global `darkDesaturation` reduction is **not** applied on top. |
@@ -573,6 +574,61 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
573
574
  | `role` | `RoleInput` | Semantic role against `base` (`'text'` / `'surface'` / `'border'` or an alias). Fixes APCA contrast polarity. Resolved via: explicit `role` → name inference → opposite of the base's role → `'text'`. See [Roles](#roles). |
574
575
  | `inherit` | `boolean` | Whether this color is inherited by child themes via `extend()`. Default: `true`. Set to `false` to make the color local to the current theme. |
575
576
 
577
+ #### `from` (a literal color)
578
+
579
+ Most of Glaze answers the question "design me a palette". `from` answers the
580
+ other one: **"honor this color."** White-label products, multi-tenant branding
581
+ and imported design tokens all arrive with a value already chosen, and it is a
582
+ contract rather than a starting point.
583
+
584
+ ```ts
585
+ const theme = glaze(280, 80);
586
+
587
+ theme.colors({
588
+ surface: { tone: 100, saturation: 0.12 },
589
+ brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
590
+ });
591
+ ```
592
+
593
+ It accepts the same values as [`glaze.color()`](#input-forms) and supplies three
594
+ things at once: `hue`, `tone`, and — uniquely among theme colors — an
595
+ **absolute saturation**.
596
+
597
+ That last one is the point. Every other color's `saturation` is a 0–1 factor of
598
+ the theme seed, so the seed is a ceiling: without `from`, the only way to place
599
+ a color more saturated than its theme is to re-seed the whole theme, dragging
600
+ every sibling along. A `from` color carries its own chroma and is unaffected by
601
+ the seed:
602
+
603
+ ```ts
604
+ // Identical output at every seed saturation — the color is the color.
605
+ for (const seed of [5, 50, 100]) {
606
+ glaze(280, seed).colors({ brand: { from: '#2f5bff' } }); // → #2f5bff
607
+ }
608
+ ```
609
+
610
+ **What "exactly" covers.** The **light, normal-contrast** variant reproduces the
611
+ value (a local `lightTone: false`, the same default the value-shorthand form of
612
+ `glaze.color()` applies). Dark and high contrast adapt as usual. That asymmetry
613
+ is deliberate: those are the variants a reader reaches for when the normal one
614
+ does not work for them, so readability outranks fidelity there — and a color
615
+ pinned across all four would just be a worse `mode: 'static'`.
616
+
617
+ A `contrast` floor still applies in every scheme and is still a floor, not a
618
+ target: a value that already clears it is emitted untouched, and one that misses
619
+ moves only as far as the floor. Because the floor is solved per scheme, which
620
+ scheme comes out exact depends on the color — a light brand cannot clear 3:1 on
621
+ a white page but clears it easily on a dark one.
622
+
623
+ Sibling fields win over what the value supplied, so `{ from: '#2f5bff', hue: 300 }`
624
+ keeps the saturation and tone and rotates the hue. Note that a `saturation`
625
+ written alongside `from` reverts to its usual meaning — a factor of the seed,
626
+ not of the color.
627
+
628
+ A `from` color needs no `base` and no `tone`: it is placed absolutely, so it
629
+ stands as a root on its own. Add `base` + `contrast` when it has to stay legible
630
+ against something.
631
+
576
632
  #### Tone values
577
633
 
578
634
  `tone` (0–100) replaces authored OKHSL lightness with a contrast-shaped axis.
@@ -599,7 +655,7 @@ delta is kept and clamped on the authored side.
599
655
 
600
656
  **Extreme tone** (`'max'` / `'min'`) forces the color to the scheme's tone extreme without a contrast hack or a magic number. `'max'` resolves to author tone 100 and `'min'` to 0. Without a `base`, both flow through scheme mapping like an absolute tone, so under `mode: 'auto'` they invert in dark (`'max'` is lightest in light, darkest in dark). Use `mode: 'static'` to pin the same extreme across schemes, or `mode: 'fixed'` to keep the same end without inverting. No `base` required.
601
657
 
602
- **Extreme tone with a `base`** keeps the pair's contrast across schemes. The tone windows are asymmetric by default (`lightTone: [10, 100]`, `darkTone: [15, 95]`), so re-mapping the extreme through the dark window would squeeze the base-to-extreme span and cost contrast. Instead Glaze measures the tone shift the light scheme applied between the base and the extreme, then replays it against the base's resolved dark tone — mirrored under `mode: 'auto'` (both ends invert), same-signed under `'fixed'`. The result is clamped to `[0, 100]` only, so it may cross the `darkTone` window edge; that is intentional, since the author asked for the extreme. When the shift does not fit above or below the dark base, the color pins at tone 100 / 0. `mode: 'static'` is unaffected, and high-contrast variants already use the full range.
658
+ **Extreme tone with a `base`** keeps the pair's contrast across schemes. The tone windows are asymmetric by default (`lightTone: [10, 100]`, `darkTone: [15, 95]`), so re-mapping the extreme through the dark window would squeeze the base-to-extreme span and cost contrast. Instead Glaze measures the tone shift the light scheme applied between the base and the extreme, then replays it against the base's resolved dark tone — mirrored under `mode: 'auto'` (both ends invert), same-signed under `'fixed'`. The result is clamped to `[0, 100]` only, so it may cross the `darkTone` window edge; that is intentional, since the author asked for the extreme. When the shift does not fit above or below the dark base, the color pins at tone 100 / 0. `mode: 'static'` is unaffected. High-contrast variants follow the same rule, but since their window is already the full range the replay reproduces the plain mapping unless the base itself sits asymmetrically across schemes.
603
659
 
604
660
  ```ts
605
661
  theme.colors({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/glaze",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "OKHST-based color theme generator with WCAG contrast solving for light, dark, and high-contrast schemes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",