diff --git a/.changeset/cool-lions-glow.md b/.changeset/cool-lions-glow.md new file mode 100644 index 0000000..0780600 --- /dev/null +++ b/.changeset/cool-lions-glow.md @@ -0,0 +1,5 @@ +--- +'@tenphi/glaze': patch +--- + +Add `inherit` flag to color definitions to prevent inheritance during `extend()` diff --git a/src/glaze.test.ts b/src/glaze.test.ts index 59d5a11..ca15378 100644 --- a/src/glaze.test.ts +++ b/src/glaze.test.ts @@ -688,6 +688,69 @@ describe('glaze', () => { expect(resolved.has('surface')).toBe(true); expect(resolved.get('fill')!.mode).toBe('fixed'); }); + + it('excludes colors with inherit: false', () => { + const primary = glaze(280, 80); + primary.colors({ + surface: { lightness: 97 }, + internalFill: { lightness: 52, inherit: false }, + text: { base: 'surface', contrast: 'AAA' }, + }); + + const child = primary.extend({ hue: 23 }); + expect(child.has('surface')).toBe(true); + expect(child.has('text')).toBe(true); + expect(child.has('internalFill')).toBe(false); + }); + + it('allows re-providing a non-inherited color via extend colors', () => { + const primary = glaze(280, 80); + primary.colors({ + surface: { lightness: 97 }, + fill: { lightness: 52, inherit: false }, + }); + + const child = primary.extend({ + hue: 23, + colors: { + fill: { lightness: 60 }, + }, + }); + + expect(child.has('fill')).toBe(true); + const resolved = child.resolve(); + expect(resolved.has('fill')).toBe(true); + }); + + it('inherit: false survives export/from round-trip', () => { + const primary = glaze(280, 80); + primary.colors({ + surface: { lightness: 97 }, + local: { lightness: 50, inherit: false }, + }); + + const exported = primary.export(); + const restored = glaze.from(exported); + + expect(restored.has('local')).toBe(true); + + const child = restored.extend({ hue: 100 }); + expect(child.has('surface')).toBe(true); + expect(child.has('local')).toBe(false); + }); + + it('throws when a dependent of a non-inherited color is resolved', () => { + const primary = glaze(280, 80); + primary.colors({ + surface: { lightness: 97, inherit: false }, + text: { base: 'surface', contrast: 'AAA' }, + }); + + const child = primary.extend({ hue: 23 }); + expect(child.has('surface')).toBe(false); + expect(child.has('text')).toBe(true); + expect(() => child.resolve()).toThrow(/non-existent base/); + }); }); describe('token export', () => { diff --git a/src/glaze.ts b/src/glaze.ts index 32f5663..217cd22 100644 --- a/src/glaze.ts +++ b/src/glaze.ts @@ -1157,9 +1157,17 @@ function createTheme( extend(options: GlazeExtendOptions): GlazeTheme { const newHue = options.hue ?? hue; const newSat = options.saturation ?? saturation; + + const inheritedColors: ColorMap = {}; + for (const [name, def] of Object.entries(colorDefs)) { + if (def.inherit !== false) { + inheritedColors[name] = def; + } + } + const mergedColors = options.colors - ? { ...colorDefs, ...options.colors } - : { ...colorDefs }; + ? { ...inheritedColors, ...options.colors } + : { ...inheritedColors }; return createTheme(newHue, newSat, mergedColors); }, diff --git a/src/types.ts b/src/types.ts index cf35519..98b185b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,6 +78,12 @@ export interface RegularColorDef { * should not be combined (a console.warn is emitted). */ opacity?: number; + + /** + * Whether this color is inherited by child themes created via `extend()`. + * Default: true. Set to false to make this color local to the current theme. + */ + inherit?: boolean; } /** Shadow tuning knobs. All values use the 0–1 scale (OKHSL). */ @@ -124,6 +130,12 @@ export interface ShadowColorDef { intensity: HCPair; /** Override default tuning. Merged field-by-field with global `shadowTuning`. */ tuning?: ShadowTuning; + + /** + * Whether this color is inherited by child themes created via `extend()`. + * Default: true. Set to false to make this color local to the current theme. + */ + inherit?: boolean; } export interface MixColorDef { @@ -159,6 +171,12 @@ export interface MixColorDef { * Supports [normal, highContrast] pair. */ contrast?: HCPair; + + /** + * Whether this color is inherited by child themes created via `extend()`. + * Default: true. Set to false to make this color local to the current theme. + */ + inherit?: boolean; } export type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;