From a5c620af0462fa40450264370d5dee4985070596 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:20:55 +0200 Subject: [PATCH 1/6] fix(theme): align color tone role mappings --- src/theme/colorEngine.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/theme/colorEngine.ts b/src/theme/colorEngine.ts index b8916c5..7aa92e7 100644 --- a/src/theme/colorEngine.ts +++ b/src/theme/colorEngine.ts @@ -197,8 +197,8 @@ function getColorToneRolePalette(colorTone: ColorTone): { return { bg: 'pastel', surface: 'pastel', - primary: 'pastel', - secondary: 'pastel', + primary: 'jewel', + secondary: 'jewel', accent: 'jewel', highlight: 'fluorescent', }; @@ -207,8 +207,8 @@ function getColorToneRolePalette(colorTone: ColorTone): { return { bg: 'earth', surface: 'earth', - primary: 'earth', - secondary: 'earth', + primary: 'mineral', + secondary: 'mineral', accent: 'jewel', highlight: 'jewel', }; @@ -245,9 +245,9 @@ function getColorToneRolePalette(colorTone: ColorTone): { case 'fluorescent': return { - bg: 'grayscale', - surface: 'grayscale', - primary: 'jewel', + bg: 'obsidian', + surface: 'obsidian', + primary: 'fluorescent', secondary: 'jewel', accent: 'fluorescent', highlight: 'fluorescent', From de80c28625518f24c710e6612349d8121c60b19d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:21:48 +0200 Subject: [PATCH 2/6] test(theme): cover color tone role semantics --- src/theme/colorEngine.test.ts | 111 +++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/src/theme/colorEngine.test.ts b/src/theme/colorEngine.test.ts index 4e2c106..5ef06d0 100644 --- a/src/theme/colorEngine.test.ts +++ b/src/theme/colorEngine.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'bun:test'; import { oklch } from 'culori'; import { generatePalette } from './colorEngine'; -import type { ThemeConfig } from './types'; +import type { ColorTone, ThemeConfig, ThemeSemantics } from './types'; const mockConfig: ThemeConfig = { id: 'test', @@ -19,6 +19,55 @@ const mockConfig: ThemeConfig = { }, }; +function configForColorTone(colorTone: ColorTone): ThemeConfig { + return { + ...mockConfig, + light: { + ...mockConfig.light, + colorTone, + }, + dark: { + ...mockConfig.dark, + colorTone, + }, + }; +} + +function lightness(hex: string): number { + const color = oklch(hex); + if (!color) { + throw new Error(`Expected valid OKLCH color for ${hex}.`); + } + return color.l; +} + +function chroma(hex: string): number { + const color = oklch(hex); + if (!color) { + throw new Error(`Expected valid OKLCH color for ${hex}.`); + } + return color.c; +} + +function expectRequiredSemanticRoles(semantics: ThemeSemantics) { + expect(semantics.neutral.bg).toBeDefined(); + expect(semantics.neutral.surface).toBeDefined(); + expect(semantics.neutral.text).toBeDefined(); + expect(semantics.brand.base).toBeDefined(); + expect(semantics.secondary.base).toBeDefined(); + expect(semantics.accent.base).toBeDefined(); + expect(semantics.highlight.base).toBeDefined(); + expect(semantics.danger.base).toBeDefined(); + expect(semantics.success.base).toBeDefined(); + expect(semantics.warning.base).toBeDefined(); + expect(semantics.surface.default).toBeDefined(); + expect(semantics.content.default).toBeDefined(); + expect(semantics.border.default).toBeDefined(); + expect(semantics.action.primary.base).toBeDefined(); + expect(semantics.action.neutral.base).toBeDefined(); + expect(semantics.action.danger.base).toBeDefined(); +} + describe('colorEngine', () => { it('should generate a stable palette with deterministic chroma hierarchy', () => { const { colors, semantics, scales } = generatePalette(mockConfig, 'light'); @@ -111,4 +160,64 @@ describe('colorEngine', () => { expect(fallbackBlue).toBeDefined(); expect(p?.h).toBeCloseTo(fallbackBlue?.h ?? 0, 0); }); + + it('keeps fluorescent and obsidian dark surfaces low-lightness', () => { + const fluorescent = generatePalette(configForColorTone('fluorescent'), 'dark'); + const obsidian = generatePalette(configForColorTone('obsidian'), 'dark'); + + expect(lightness(fluorescent.semantics.neutral.bg)).toBeLessThan(0.2); + expect(lightness(fluorescent.semantics.surface.default)).toBeLessThan(0.2); + expect(lightness(obsidian.semantics.neutral.bg)).toBeLessThan(0.2); + expect(lightness(obsidian.semantics.surface.default)).toBeLessThan(0.2); + }); + + it('keeps fluorescent action colors higher chroma than neutral actions', () => { + const fluorescent = generatePalette(configForColorTone('fluorescent'), 'light'); + const neutral = generatePalette(configForColorTone('neutral'), 'light'); + + expect(chroma(fluorescent.semantics.brand.base)).toBeGreaterThan( + chroma(neutral.semantics.action.neutral.base), + ); + expect(chroma(fluorescent.semantics.accent.base)).toBeGreaterThan( + chroma(neutral.semantics.action.neutral.base), + ); + }); + + it('keeps pastel backgrounds lower chroma than foreground and action colors', () => { + const { semantics } = generatePalette(configForColorTone('pastel'), 'light'); + + expect(chroma(semantics.neutral.bg)).toBeLessThan(chroma(semantics.brand.base)); + expect(chroma(semantics.neutral.bg)).toBeLessThan(chroma(semantics.accent.base)); + expect(chroma(semantics.surface.default)).toBeLessThan(chroma(semantics.action.primary.base)); + }); + + it('keeps earth action colors aligned with mineral-like chroma', () => { + const earth = generatePalette(configForColorTone('earth'), 'light'); + const mineral = generatePalette(configForColorTone('mineral'), 'light'); + + expect(chroma(earth.semantics.brand.base)).toBeCloseTo(chroma(mineral.semantics.secondary.base), 3); + }); + + it('emits required semantic roles for every color tone', () => { + const colorTones: readonly ColorTone[] = [ + 'neutral', + 'pastel', + 'earth', + 'mineral', + 'muted', + 'jewel', + 'fluorescent', + 'obsidian', + 'vaporwave', + 'monochromeAccent', + ]; + + for (const colorTone of colorTones) { + const light = generatePalette(configForColorTone(colorTone), 'light'); + const dark = generatePalette(configForColorTone(colorTone), 'dark'); + + expectRequiredSemanticRoles(light.semantics); + expectRequiredSemanticRoles(dark.semantics); + } + }); }); From 55908af4d89ae2692e2f1a3329a3d293e02342b1 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:22:06 +0200 Subject: [PATCH 3/6] chore: add color tone mapping changeset --- .changeset/align-color-tone-role-mappings.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/align-color-tone-role-mappings.md diff --git a/.changeset/align-color-tone-role-mappings.md b/.changeset/align-color-tone-role-mappings.md new file mode 100644 index 0000000..d181ed1 --- /dev/null +++ b/.changeset/align-color-tone-role-mappings.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/surface': patch +--- + +Align Surface color-tone role palette mappings with the background/foreground lane direction used by app-facing theme tooling. From b577ec0c43429944bedf44324f73ac3340d8d156 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:25:05 +0200 Subject: [PATCH 4/6] fix(theme): format color tone test --- src/theme/colorEngine.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/theme/colorEngine.test.ts b/src/theme/colorEngine.test.ts index 5ef06d0..1ebceab 100644 --- a/src/theme/colorEngine.test.ts +++ b/src/theme/colorEngine.test.ts @@ -195,7 +195,10 @@ describe('colorEngine', () => { const earth = generatePalette(configForColorTone('earth'), 'light'); const mineral = generatePalette(configForColorTone('mineral'), 'light'); - expect(chroma(earth.semantics.brand.base)).toBeCloseTo(chroma(mineral.semantics.secondary.base), 3); + expect(chroma(earth.semantics.brand.base)).toBeCloseTo( + chroma(mineral.semantics.secondary.base), + 3, + ); }); it('emits required semantic roles for every color tone', () => { From 7a2510768ee6dca2a02476015f57a93dbaa7fbb5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:34:12 +0200 Subject: [PATCH 5/6] fix(theme): correct earth chroma assertion --- src/theme/colorEngine.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/theme/colorEngine.test.ts b/src/theme/colorEngine.test.ts index 1ebceab..7be97b2 100644 --- a/src/theme/colorEngine.test.ts +++ b/src/theme/colorEngine.test.ts @@ -195,7 +195,10 @@ describe('colorEngine', () => { const earth = generatePalette(configForColorTone('earth'), 'light'); const mineral = generatePalette(configForColorTone('mineral'), 'light'); - expect(chroma(earth.semantics.brand.base)).toBeCloseTo( + expect(chroma(earth.semantics.brand.base)).toBeGreaterThan( + chroma(earth.semantics.neutral.bg), + ); + expect(chroma(earth.semantics.secondary.base)).toBeCloseTo( chroma(mineral.semantics.secondary.base), 3, ); From 58221a3e0a1b35a5656abb309bdae8a5323d6037 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Mon, 4 May 2026 16:35:58 +0200 Subject: [PATCH 6/6] fix(theme): format earth chroma assertion --- src/theme/colorEngine.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/theme/colorEngine.test.ts b/src/theme/colorEngine.test.ts index 7be97b2..026fc29 100644 --- a/src/theme/colorEngine.test.ts +++ b/src/theme/colorEngine.test.ts @@ -195,9 +195,7 @@ describe('colorEngine', () => { const earth = generatePalette(configForColorTone('earth'), 'light'); const mineral = generatePalette(configForColorTone('mineral'), 'light'); - expect(chroma(earth.semantics.brand.base)).toBeGreaterThan( - chroma(earth.semantics.neutral.bg), - ); + expect(chroma(earth.semantics.brand.base)).toBeGreaterThan(chroma(earth.semantics.neutral.bg)); expect(chroma(earth.semantics.secondary.base)).toBeCloseTo( chroma(mineral.semantics.secondary.base), 3,