From 28e41a96dc828a8b8c8b066da8d16443dd5256d0 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:45:18 +0200 Subject: [PATCH 1/7] add reusable inverse surface semantic --- src/theme/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/theme/types.ts b/src/theme/types.ts index a4e4383..1fd14f3 100644 --- a/src/theme/types.ts +++ b/src/theme/types.ts @@ -43,6 +43,7 @@ export interface SurfaceSemantics { default: string; subtle: string; raised: string; + inverse: string; } export interface ContentSemantics { From f1ca0280be7f1b05f9c0a541ed2d93658b58ee61 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:45:42 +0200 Subject: [PATCH 2/7] pair inverse surface and content semantics --- src/theme/colorEngine.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/theme/colorEngine.ts b/src/theme/colorEngine.ts index b465134..864e88e 100644 --- a/src/theme/colorEngine.ts +++ b/src/theme/colorEngine.ts @@ -120,7 +120,6 @@ export function generatePalette( parseHexColorOrThrow(modeConfig.primaryColor); const generated = generateThemeModeColors(modeConfig); - const { swatches } = generated; const neutralSwatch = swatches.neutral; @@ -140,18 +139,21 @@ export function generatePalette( // Keep destructive action and feedback error aligned until a dedicated error status swatch is introduced. const error = danger; const info = buildRoleSemantics(secondarySwatch, isDark); + const inverseSurface = isDark ? neutralSwatch[50] : neutralSwatch[900]; + const inverseOnSurface = isDark ? neutralSwatch[900] : neutralSwatch[50]; const surfaceSemantics: SurfaceSemantics = { default: neutral.surface, subtle: neutral.bgSubtle, raised: neutral.surface, + inverse: inverseSurface, }; const content: ContentSemantics = { default: neutral.text, muted: neutral.textMuted, subtle: neutral.textSubtle, - inverse: brand.onSolidText, + inverse: inverseOnSurface, }; const border: BorderSemantics = { From 23b8b61e73829d52318fea9c4465795e3326577f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:45:53 +0200 Subject: [PATCH 3/7] use inverse surface semantic for tooltips --- src/components/tooltip/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tooltip/Tooltip.tsx b/src/components/tooltip/Tooltip.tsx index 1294822..5dce7c4 100644 --- a/src/components/tooltip/Tooltip.tsx +++ b/src/components/tooltip/Tooltip.tsx @@ -93,7 +93,7 @@ export function Tooltip({ Date: Tue, 21 Jul 2026 02:46:13 +0200 Subject: [PATCH 4/7] test reusable inverse color semantics --- src/theme/inverseSemantics.test.ts | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/theme/inverseSemantics.test.ts diff --git a/src/theme/inverseSemantics.test.ts b/src/theme/inverseSemantics.test.ts new file mode 100644 index 0000000..2b47091 --- /dev/null +++ b/src/theme/inverseSemantics.test.ts @@ -0,0 +1,48 @@ +import type { ThemeConfig } from '@ankhorage/contracts'; +import { describe, expect, it } from 'bun:test'; + +import { generatePalette } from './colorEngine'; + +const config: ThemeConfig = { + id: 'inverse-semantics-test', + name: 'Inverse semantics test', + light: { + primaryColor: '#3B82F6', + harmony: 'monochromatic', + }, + dark: { + primaryColor: '#3B82F6', + harmony: 'monochromatic', + }, +}; + +describe('inverse semantics', () => { + it('provides a paired inverse surface and content color in light mode', () => { + const { semantics, swatches } = generatePalette(config, 'light'); + + expect(semantics.surface.inverse).toBe(swatches.neutral[900]); + expect(semantics.content.inverse).toBe(swatches.neutral[50]); + }); + + it('provides a paired inverse surface and content color in dark mode', () => { + const { semantics, swatches } = generatePalette(config, 'dark'); + + expect(semantics.surface.inverse).toBe(swatches.neutral[50]); + expect(semantics.content.inverse).toBe(swatches.neutral[900]); + }); + + it('resolves global inverse content from neutral semantics instead of brand foreground', () => { + const brightBrandConfig: ThemeConfig = { + ...config, + light: { + ...config.light, + primaryColor: '#FDE047', + }, + }; + const { semantics, swatches } = generatePalette(brightBrandConfig, 'light'); + + expect(semantics.surface.inverse).toBe(swatches.neutral[900]); + expect(semantics.content.inverse).toBe(swatches.neutral[50]); + expect(semantics.content.inverse).not.toBe(semantics.brand.onSolidText); + }); +}); From 7a63a046a1aae660c9d9578f0e5942dbfc40c16b Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:46:18 +0200 Subject: [PATCH 5/7] add inverse semantics changeset --- .changeset/fix-inverse-tooltip-semantics.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-inverse-tooltip-semantics.md diff --git a/.changeset/fix-inverse-tooltip-semantics.md b/.changeset/fix-inverse-tooltip-semantics.md new file mode 100644 index 0000000..05bc9b7 --- /dev/null +++ b/.changeset/fix-inverse-tooltip-semantics.md @@ -0,0 +1,5 @@ +--- +"@ankhorage/surface": patch +--- + +Add reusable inverse surface/content semantics and use the paired colors for readable mode-aware tooltips. From c141b0408365be9aa168d9fd3bce34e54552538b Mon Sep 17 00:00:00 2001 From: artiphishle Date: Tue, 21 Jul 2026 02:49:06 +0200 Subject: [PATCH 6/7] docs: update --- .changeset/fix-inverse-tooltip-semantics.md | 2 +- paradox/exports.json | 21 ++++++++++++------- paradox/exports.md | 15 +++++++------- paradox/index.html | 21 ++++++++++++------- paradox/paradox.json | 23 ++++++++++++++------- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/.changeset/fix-inverse-tooltip-semantics.md b/.changeset/fix-inverse-tooltip-semantics.md index 05bc9b7..752167c 100644 --- a/.changeset/fix-inverse-tooltip-semantics.md +++ b/.changeset/fix-inverse-tooltip-semantics.md @@ -1,5 +1,5 @@ --- -"@ankhorage/surface": patch +'@ankhorage/surface': patch --- Add reusable inverse surface/content semantics and use the paired colors for readable mode-aware tooltips. diff --git a/paradox/exports.json b/paradox/exports.json index 0fd479b..2916e9e 100644 --- a/paradox/exports.json +++ b/paradox/exports.json @@ -8,7 +8,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 61, + "line": 62, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -756,7 +756,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 55, + "line": 56, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -3604,7 +3604,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 48, + "line": 49, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -4572,7 +4572,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 84, + "line": 85, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -10160,6 +10160,13 @@ "required": true, "description": null }, + { + "name": "inverse", + "kind": "property", + "type": "string", + "required": true, + "description": null + }, { "name": "raised", "kind": "property", @@ -10204,7 +10211,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 186, + "line": 187, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -14152,7 +14159,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 67, + "line": 68, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -14276,7 +14283,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 97, + "line": 98, "column": 1 }, "exportPaths": ["src/index.ts"], diff --git a/paradox/exports.md b/paradox/exports.md index cabb6e7..3389096 100644 --- a/paradox/exports.md +++ b/paradox/exports.md @@ -4,7 +4,7 @@ Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:61:1` +Source: `src/theme/types.ts:62:1` ### Members @@ -252,7 +252,7 @@ Source: `src/components/badge/types.ts:7:1` Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:55:1` +Source: `src/theme/types.ts:56:1` ### Members @@ -926,7 +926,7 @@ Source: `src/layout/Container.tsx:10:1` Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:48:1` +Source: `src/theme/types.ts:49:1` ### Members @@ -1245,7 +1245,7 @@ Source: `src/context/FontContext.tsx:6:1` Kind: `unknown` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:84:1` +Source: `src/theme/types.ts:85:1` ## GeneratedColorRole @@ -2787,6 +2787,7 @@ Source: `src/theme/types.ts:42:1` | Name | Kind | Type | Required | Description | | ------- | -------- | -------- | -------- | ----------- | | default | property | `string` | yes | | +| inverse | property | `string` | yes | | | raised | property | `string` | yes | | | subtle | property | `string` | yes | | @@ -2800,7 +2801,7 @@ Source: `src/surfaceColor.ts:20:1` Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:186:1` +Source: `src/theme/types.ts:187:1` ### Members @@ -3680,7 +3681,7 @@ Source: `src/theme/ThemeContext.tsx:31:14` Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:67:1` +Source: `src/theme/types.ts:68:1` ### Members @@ -3705,7 +3706,7 @@ Source: `src/theme/types.ts:67:1` Kind: `type` Module: `src/theme/types.ts` -Source: `src/theme/types.ts:97:1` +Source: `src/theme/types.ts:98:1` ### Members diff --git a/paradox/index.html b/paradox/index.html index 7325838..93e47d5 100644 --- a/paradox/index.html +++ b/paradox/index.html @@ -19849,7 +19849,7 @@

src/theme/types.ts

data-search="ActionSemantics type src/theme/types.ts RoleSemantics" >

ActionSemantics

-

type • src/theme/types.ts:61:1

+

type • src/theme/types.ts:62:1

Export paths: src/index.ts

@@ -19900,7 +19900,7 @@

ActionSemantics

data-search="BorderSemantics type src/theme/types.ts " >

BorderSemantics

-

type • src/theme/types.ts:55:1

+

type • src/theme/types.ts:56:1

Export paths: src/index.ts

Related symbols: None
@@ -19946,7 +19946,7 @@

BorderSemantics

data-search="ContentSemantics type src/theme/types.ts " >

ContentSemantics

-

type • src/theme/types.ts:48:1

+

type • src/theme/types.ts:49:1

Export paths: src/index.ts

Related symbols: None
@@ -19999,7 +19999,7 @@

ContentSemantics

data-search="FontWeight unknown src/theme/types.ts " >

FontWeight

-

unknown • src/theme/types.ts:84:1

+

unknown • src/theme/types.ts:85:1

Export paths: src/index.ts

Related symbols: None
@@ -20216,6 +20216,13 @@

SurfaceSemantics

yes + + inverse + property + string + yes + + raised property @@ -20239,7 +20246,7 @@

SurfaceSemantics

data-search="SurfaceTheme type src/theme/types.ts FontWeight GeneratedThemeSwatches ThemeSemantics" >

SurfaceTheme

-

type • src/theme/types.ts:186:1

+

type • src/theme/types.ts:187:1

Export paths: src/index.ts

@@ -20361,7 +20368,7 @@

SurfaceTheme

data-search="ThemeSemantics type src/theme/types.ts ActionSemantics BorderSemantics ContentSemantics NeutralSemantics RoleSemantics SurfaceSemantics" >

ThemeSemantics

-

type • src/theme/types.ts:67:1

+

type • src/theme/types.ts:68:1

Export paths: src/index.ts

@@ -20494,7 +20501,7 @@

ThemeSemantics

data-search="ThemeTokens type src/theme/types.ts FontWeight GeneratedThemeSwatches ThemeSemantics" >

ThemeTokens

-

type • src/theme/types.ts:97:1

+

type • src/theme/types.ts:98:1

Export paths: src/index.ts

diff --git a/paradox/paradox.json b/paradox/paradox.json index 76c3f22..1a66598 100644 --- a/paradox/paradox.json +++ b/paradox/paradox.json @@ -1685,7 +1685,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 61, + "line": 62, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -2433,7 +2433,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 55, + "line": 56, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -5281,7 +5281,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 48, + "line": 49, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -6249,7 +6249,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 84, + "line": 85, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -11837,6 +11837,13 @@ "required": true, "description": null }, + { + "name": "inverse", + "kind": "property", + "type": "string", + "required": true, + "description": null + }, { "name": "raised", "kind": "property", @@ -11881,7 +11888,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 186, + "line": 187, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -15829,7 +15836,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 67, + "line": 68, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -15953,7 +15960,7 @@ "modulePath": "src/theme/types.ts", "sourceLocation": { "filePath": "src/theme/types.ts", - "line": 97, + "line": 98, "column": 1 }, "exportPaths": ["src/index.ts"], @@ -35953,7 +35960,7 @@ { "fromComponent": "Tooltip", "toComponent": "Surface", - "jsxElement": "", + "jsxElement": "", "sourcePath": "src/components/tooltip/Tooltip.tsx" }, { From dd9101343f3334be4247859090016d15df699812 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:50:10 +0200 Subject: [PATCH 7/7] test: update dark palette inverse semantics assertion --- src/theme/colorEngine.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/theme/colorEngine.test.ts b/src/theme/colorEngine.test.ts index 8fd6363..64a030f 100644 --- a/src/theme/colorEngine.test.ts +++ b/src/theme/colorEngine.test.ts @@ -67,10 +67,11 @@ describe('colorEngine', () => { }); it('generates a valid palette for dark mode', () => { - const { colors, semantics } = generatePalette(mockConfig, 'dark'); + const { colors, swatches, semantics } = generatePalette(mockConfig, 'dark'); expect(colors.background).toBe(semantics.neutral.bg); - expect(semantics.content.inverse).toBe(semantics.brand.onSolidText); + expect(semantics.surface.inverse).toBe(swatches.neutral[50]); + expect(semantics.content.inverse).toBe(swatches.neutral[900]); }); it('uses the color-theory default semantic status seeds', () => {