Skip to content

Latest commit

 

History

History
332 lines (254 loc) · 19.2 KB

theme.md

File metadata and controls

332 lines (254 loc) · 19.2 KB
title Themes
description Create themes with glaze(), restore them from snapshots, and manage their color definitions with the theme methods.

A theme is one hue/saturation seed plus a map of named color definitions. Glaze resolves every definition against the seed into four scheme variants (light, dark, lightContrast, darkContrast). For a guided introduction, see Getting started and Designing a palette.

Creating a theme

Call Description
glaze(hue, saturation?, config?) Theme from a hue (0–360) and saturation (0–100). saturation defaults to 100.
glaze(seed, config?) Theme from a GlazeThemeSeed object, which can also carry a dark seed.
glaze.fromHex(hex) Theme seeded with the hue and saturation of a hex color.
glaze.fromRgb(r, g, b) Theme seeded with the hue and saturation of an sRGB color (0–255 per channel).
glaze.themeFrom(data) Theme restored from a theme.export() snapshot.
glaze.from(data) Alias for glaze.themeFrom, kept for compatibility.
import { glaze } from '@tenphi/glaze';

const a = glaze(280, 80);
const b = glaze({ hue: 280, saturation: 80 });
const c = glaze({
  hue: 280,
  saturation: 80,
  darkHue: 268,
  darkSaturation: 65,
});
const d = glaze.fromHex('#7a4dbf');
const e = glaze.fromRgb(122, 77, 191);
const f = glaze.themeFrom(a.export());

// Per-theme config override (last argument in both forms):
const raw = glaze(280, 80, { lightTone: false, darkTone: false });
const rawSeed = glaze({ hue: 280, saturation: 80 }, { pastel: true });

glaze(hue, saturation?, config?)

Parameter Type Description
hue number Hue seed, 0–360.
saturation number Saturation seed, 0–100. Default 100.
config GlazeConfigOverride Optional per-theme config override. See Per-theme config override.

glaze(seed, config?)

The object form takes a GlazeThemeSeed first and an optional GlazeConfigOverride second. The older spelling glaze(seed, undefined, config) still works but is deprecated.

GlazeThemeSeed

Field Type Description
hue number Light-scheme hue seed, 0–360. Required.
saturation number Light-scheme saturation seed, 0–100. Required.
darkHue number Hue seed for the dark and darkContrast variants, 0–360. Defaults to hue.
darkSaturation number Saturation seed for the dark and darkContrast variants, 0–100. Defaults to saturation. When set, the global darkDesaturation reduction is not applied on top. A from color without its own saturation or darkSaturation doesn't read this seed, so it is still reduced.

How a dark seed combines with per-color hue / darkHue / saturation / darkSaturation is in Color definitions; the concept is in Dark seed.

glaze.fromHex(hex) / glaze.fromRgb(r, g, b)

Both convert the color to OKHSL and seed the theme with its hue and saturation (OKHSL saturation × 100). The color's lightness is discarded: tones come from the color definitions you add.

  • fromHex accepts #rgb or #rrggbb; the leading # is optional. Hex with alpha (#rgba, #rrggbbaa) and any other string throw glaze: invalid hex color "<hex>". (glaze.color() and from do accept alpha hex and drop the alpha with a warning.)
  • fromRgb takes gamma-encoded sRGB channels on 0–255.
  • Neither accepts a config override. Use glaze(theme.hue, theme.saturation, config) with the extracted seed if you need one.
const brand = glaze.fromHex('#7a4dbf');
brand.hue; // ≈ 298.52
brand.saturation; // ≈ 70.41

To keep a literal color's exact tone and saturation instead of only its hue, use the from field on a color definition or a standalone glaze.color() token.

glaze.themeFrom(data) / glaze.from(data)

Restores a theme from a theme.export() snapshot (GlazeThemeExport). The restored theme pins the snapshot's frozen config as its own override. glaze.from is the same function.

themeFrom throws when:

Condition Error message
data is not an object glaze.themeFrom: expected an object from theme.export(), got <type>.
kind is present and not 'theme' glaze.themeFrom: expected kind "theme", got "<kind>".
version is present and not an integer ≥ 1 glaze.themeFrom: invalid "version" field — expected an integer >= 1 (got <value>).
version is greater than GLAZE_EXPORT_VERSION glaze.themeFrom: unsupported export version <n> (this library supports version 2). Upgrade @tenphi/glaze to load this snapshot.
version is 1 and the snapshot has shadow colors or a shadowTuning glaze.themeFrom: this snapshot is export version 1 (Glaze 1.1–2) and contains … — see Upgrading to 3.0.
hue or saturation is not a number glaze.themeFrom: expected numeric "hue" and "saturation" fields.
darkHue / darkSaturation is present and not a number glaze.themeFrom: expected "darkHue" to be a number when present (got <value>).

Color definitions are not validated on restore; they are validated when the theme first resolves. The versioning rules shared by all snapshot kinds are in Authoring snapshots.

Per-theme config override

The optional config argument is a GlazeConfigOverride — the resolve-relevant subset of the global config plus the instance-only pastel flag. Its fields are listed in GlazeConfigOverride.

  • The override is merged over the live global config at resolve time. Fields the override doesn't set keep following later glaze.configure() calls.
  • The override is copied when the theme is created (and by extend()), so changing the object or array you passed afterwards has no effect. Its fields are read by property access, so a class instance with getters or a proxy works as an override. Validation still happens at resolve.
  • pastel is instance-only: it comes from the override (or a per-color pastel) and defaults to false; glaze.configure() can't set it.
  • modes and states are not part of the override. They are global, or per call on the output methods.

Theme properties

Member Type Description
theme.hue number (readonly) The hue seed, 0–360.
theme.saturation number (readonly) The saturation seed, 0–100.
theme.darkHue number | undefined (readonly) The dark-scheme hue seed, or undefined when the theme reuses hue.
theme.darkSaturation number | undefined (readonly) The dark-scheme saturation seed, or undefined when the theme reuses saturation.
theme.getConfig() GlazeConfigResolved The effective config this theme resolves with. See theme.getConfig().

Theme methods

Method Description
theme.colors(defs) Add or replace color definitions. Additive merge: adds new names, overwrites existing ones, keeps the rest.
theme.color(name) Get one color definition, or undefined.
theme.color(name, def) Set one color definition.
theme.remove(name | names[]) Remove one or more color definitions.
theme.has(name) Whether a color is defined.
theme.list() Defined color names, in insertion order.
theme.reset() Remove every color definition.
theme.extend(options) Create a child theme that inherits the color definitions.
theme.resolve() Resolve every color and return a Map<string, ResolvedColor>.
theme.export(override?) JSON-safe authoring snapshot, restorable with glaze.themeFrom().
theme.getConfig() The effective resolved config.
theme.tokens() … theme.tailwind() Output methods — see Output methods.

theme.colors(defs)

Takes a ColorMap (Record<string, ColorDef>). The definition shapes are in Color definitions.

theme.colors({ surface: { tone: 97 } });
theme.colors({
  'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
});
// Both 'surface' and 'surface-text' are now defined.

theme.color(name) / theme.color(name, def)

theme.color('surface', { tone: 97, saturation: 0.75 }); // set
theme.color('surface'); // get → { tone: 97, saturation: 0.75 }
theme.color('missing'); // → undefined

The getter returns the stored definition object itself; replace it with the setter rather than mutating it, so the theme knows to re-resolve.

theme.remove(names)

theme.remove('accent');
theme.remove(['accent', 'accent-text']);

Removing a color that another color uses as base, bg, fg, or target makes the next resolve throw, for example glaze: color "surface-text" references non-existent base "surface". The full validation table is in Validation.

theme.has(name) / theme.list() / theme.reset()

theme.list(); // → ['surface', 'surface-text', 'accent']
theme.has('surface'); // → true
theme.reset();
theme.list(); // → []

theme.extend(options)

Creates a new theme that inherits the parent's color definitions, optionally replacing the seed, overriding colors, and adding config:

const danger = primary.extend({
  hue: 23,
  colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
});

// Inherit the parent's config override and widen the dark window further:
const wide = primary.extend({ config: { darkTone: [10, 100] } });

GlazeExtendOptions:

Field Type Description
hue number Replace the hue seed. Defaults to the parent's hue.
saturation number Replace the saturation seed. Defaults to the parent's saturation.
darkHue number Replace the dark hue seed. Defaults to the parent's darkHue.
darkSaturation number Replace the dark saturation seed. Defaults to the parent's darkSaturation.
colors ColorMap Color overrides for the child, merged additively over the inherited map (same as theme.colors()).
config GlazeConfigOverride Config override for the child. Shallow-merged over the parent's override; child fields win.
  • Colors defined with inherit: false on the parent are not copied into the child (see inherit).
  • The child is independent: later changes to the parent don't reach it.
  • Nested objects in config (tone windows, shadowTuning, glowTuning) replace the parent's value wholesale rather than merging field by field: a child glowTuning without headroom drops the parent's.

Building status themes with extend is walked through in Designing a palette.

theme.resolve()

Resolves the dependency graph and returns Map<string, ResolvedColor>, keyed by color name. Entries are in dependency order: definition order, except that a referenced color (base, bg, fg, target) not yet placed is inserted just before the first color that references it. With a based on c and defined as a, b, c, the order is c, a, b. The output methods emit tokens in the same order. Call resolve() directly for tests, diagnostics, or a custom output pipeline.

const resolved = theme.resolve();
const surface = resolved.get('surface');
// surface.light
// → { h: 280, s: 0.8, t: 0.97148…, alpha: 1, pastel: false }
// t is canonical tone on 0–1.

ResolvedColor:

Field Type Description
name string The color name.
light ResolvedColorVariant Light, normal contrast.
dark ResolvedColorVariant Dark, normal contrast.
lightContrast ResolvedColorVariant Light, high contrast.
darkContrast ResolvedColorVariant Dark, high contrast.
mode AdaptationMode | undefined The color's adaptation mode ('auto' when unset). undefined for shadow, glow, and mix colors.

All four variants are always resolved, whatever modes says; modes only filters what the output methods emit.

ResolvedColorVariant stores OKHST:

Field Type Description
h number OKHSL hue, 0–360.
s number OKHSL saturation, 0–1.
t number Canonical tone, 0–1 (reference eps).
alpha number Opacity, 0–1. 1 for opaque colors.
pastel boolean (optional) The effective pastel flag used while resolving, so formatting applies the same gamut mapping.
hdr ResolvedHdr (optional) A glow's HDR value past SDR white, { h, s, t, exposure }: an opaque base plus a gain in stops. Present only on a glow with a headroom whose step passes white; see HDR values.

Format a variant with glaze.format() and its HDR value with glaze.formatHdr(); convert tone to OKHSL lightness with variantToOkhsl from OKHST tone utilities.

Caching. A theme caches its resolved map. Changing definitions (colors, color(name, def), remove, reset) or calling glaze.configure() / glaze.resetConfig() invalidates it. resolve() returns a new Map each call, so adding or deleting entries doesn't affect the cache; the ResolvedColor objects inside are shared with it, so treat them as read-only.

theme.export(override?)

Returns a deep-cloned, JSON-safe authoring snapshot: the seed, the color definitions, and a frozen copy of the effective config — not resolved color strings. Restore it with glaze.themeFrom(). For resolved colors use the output methods; Exporting and integration compares the two.

const snapshot = theme.export();
// → {
//     kind: 'theme',
//     version: 2,
//     hue: 280,
//     saturation: 80,
//     colors: {
//       surface: { tone: 97 },
//       'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
//       accent: { tone: 52, mode: 'fixed' },
//     },
//     config: {
//       lightTone: { lo: 10, hi: 100, eps: 0.05 },
//       darkTone: { lo: 15, hi: 95, eps: 0.05 },
//       darkDesaturation: 0.1,
//       autoFlip: true,
//       pastel: false,
//       inferRole: true,
//     },
//   }

const restored = glaze.themeFrom(JSON.parse(JSON.stringify(snapshot)));

GlazeThemeExport:

Field Type Description
kind 'theme' Snapshot kind. Always written; optional on legacy or hand-written input.
version number Schema version, GLAZE_EXPORT_VERSION (2). Always written; optional on legacy input.
hue number Hue seed.
saturation number Saturation seed.
darkHue number Dark hue seed. Omitted when the theme reuses hue.
darkSaturation number Dark saturation seed. Omitted when the theme reuses saturation.
colors ColorMap Deep clone of the color definitions.
config GlazeConfigOverride Frozen effective config (below). May be sparse on legacy input; omitted fields then follow the live global config.

What the config freeze contains. export() merges, later wins: the live global config, then the theme's own override, then the override argument. From that merge it writes:

Field Frozen
lightTone, darkTone, darkDesaturation, autoFlip, pastel, inferRole Always.
shadowTuning, glowTuning When set anywhere in the merge. A nested tuning object replaces the earlier one wholesale. A glowTuning.headroom is written clamped to 0–2.
contrastLevel Only when the theme's own override or the override argument sets it. A global level is a live user preference (an accessibility slider), so it is never frozen.
modes, states Never — they are export-time choices, not theme data.

A restored theme pins the frozen fields as its override, so later glaze.configure() calls no longer change them; unfrozen fields keep following the global config. The override argument affects only the snapshot, not the live theme.

glaze.configure({ contrastLevel: 40 });
theme.export().config?.contrastLevel;
// → undefined (a global level is not frozen)
theme.export({ contrastLevel: 70 }).config?.contrastLevel;
// → 70

theme.getConfig()

Returns the effective GlazeConfigResolved the theme resolves with: the live global config with the theme's override merged on top. pastel comes only from the override (default false); states and modes are always the global values. The field list is in GlazeConfigResolved.

The result is a deep copy: mutating it, nested windows, states, modes, and tunings included, changes nothing about the theme or the global config.

const t = glaze(280, 80, { darkTone: [10, 100], pastel: true });
t.getConfig().darkTone; // → [10, 100]
t.getConfig().pastel; // → true

Output methods

Each method resolves the theme (cached) and formats the result. Full options and return shapes are in Output formats.

Method Returns
theme.tokens(options?) Flat token map grouped by scheme variant: { light: { name: value }, dark: {…} }.
theme.tasty(options?) Tasty style-to-state bindings: { '#name': { '': value, '<dark state>': value } }.
theme.json(options?) Per-color JSON: { name: { light: value, dark: value } }.
theme.css(options?) CSS custom property declarations, one string per variant (GlazeCssResult).
theme.dtcg(options?) One W3C DTCG token document per scheme variant.
theme.dtcgResolver(options?) One DTCG Resolver-Module document holding every scheme variant.
theme.tailwind(options?) A Tailwind CSS v4 @theme block plus dark / high-contrast overrides, as one CSS string.

With modes.hdr (for css(), its own hdr option, which defaults to modes.hdr), each method also writes the HDR values of glows past SDR white (glowTuning.headroom): tokens() a <scheme>Hdr map after each scheme, tasty() a states.hdr key after each SDR key that has one, json() <scheme>Hdr keys, css() result.hdr, dtcg() and dtcgResolver() $extensions['me.tenphi.glaze'].hdr ($value stays SDR), and tailwind() blocks under hdrSelector. HDR strings are extended color(srgb R G B), and the css(), tasty(), and tailwind() tiers are experimental. Off, every method's output is the same as with no headroom set. Shapes: HDR output; what each export is for: HDR glows.

To combine several themes into one token namespace, use a palette.