@ankhorage/surface 3.1.2 → 3.2.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/navigation/helpers.d.ts.map +1 -1
  3. package/dist/components/navigation/helpers.js +16 -8
  4. package/dist/components/navigation/helpers.js.map +1 -1
  5. package/dist/internal/resolvers/resolveInteractiveColors.d.ts.map +1 -1
  6. package/dist/internal/resolvers/resolveInteractiveColors.js +19 -13
  7. package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
  8. package/dist/internal/resolvers/resolveSelectionControlColors.d.ts.map +1 -1
  9. package/dist/internal/resolvers/resolveSelectionControlColors.js +19 -10
  10. package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
  11. package/dist/theme/colorEngine.d.ts +4 -3
  12. package/dist/theme/colorEngine.d.ts.map +1 -1
  13. package/dist/theme/colorEngine.js +23 -122
  14. package/dist/theme/colorEngine.js.map +1 -1
  15. package/dist/theme/createSurfaceColorDiagnosticCollector.d.ts +14 -0
  16. package/dist/theme/createSurfaceColorDiagnosticCollector.d.ts.map +1 -0
  17. package/dist/theme/createSurfaceColorDiagnosticCollector.js +53 -0
  18. package/dist/theme/createSurfaceColorDiagnosticCollector.js.map +1 -0
  19. package/dist/theme/createSurfaceSemanticPolicy.d.ts +30 -0
  20. package/dist/theme/createSurfaceSemanticPolicy.d.ts.map +1 -0
  21. package/dist/theme/createSurfaceSemanticPolicy.js +118 -0
  22. package/dist/theme/createSurfaceSemanticPolicy.js.map +1 -0
  23. package/dist/theme/createTheme.d.ts.map +1 -1
  24. package/dist/theme/createTheme.js +2 -1
  25. package/dist/theme/createTheme.js.map +1 -1
  26. package/dist/theme/defaultTokens.d.ts +1 -1
  27. package/dist/theme/defaultTokens.d.ts.map +1 -1
  28. package/dist/theme/defaultTokens.js.map +1 -1
  29. package/dist/theme/resolveGlobalTokens.d.ts +1 -1
  30. package/dist/theme/resolveGlobalTokens.d.ts.map +1 -1
  31. package/dist/theme/resolveGlobalTokens.js.map +1 -1
  32. package/dist/theme/resolveRoleSemantics.d.ts +19 -0
  33. package/dist/theme/resolveRoleSemantics.d.ts.map +1 -0
  34. package/dist/theme/resolveRoleSemantics.js +29 -0
  35. package/dist/theme/resolveRoleSemantics.js.map +1 -0
  36. package/dist/theme/resolveSurfaceSemanticModel.d.ts +16 -0
  37. package/dist/theme/resolveSurfaceSemanticModel.d.ts.map +1 -0
  38. package/dist/theme/resolveSurfaceSemanticModel.js +128 -0
  39. package/dist/theme/resolveSurfaceSemanticModel.js.map +1 -0
  40. package/dist/theme/surfaceColorPolicy.d.ts +8 -0
  41. package/dist/theme/surfaceColorPolicy.d.ts.map +1 -0
  42. package/dist/theme/surfaceColorPolicy.js +8 -0
  43. package/dist/theme/surfaceColorPolicy.js.map +1 -0
  44. package/dist/theme/types.d.ts +52 -1
  45. package/dist/theme/types.d.ts.map +1 -1
  46. package/dist/theme/types.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/components/navigation/helpers.test.ts +4 -4
  49. package/src/components/navigation/helpers.ts +17 -8
  50. package/src/index.test.ts +12 -0
  51. package/src/internal/resolvers/resolveInteractiveColors.test.ts +1 -0
  52. package/src/internal/resolvers/resolveInteractiveColors.ts +19 -13
  53. package/src/internal/resolvers/resolveSelectionControlColors.test.ts +3 -2
  54. package/src/internal/resolvers/resolveSelectionControlColors.ts +19 -10
  55. package/src/primitives/icon/PortableIcon.web.test.tsx +2 -7
  56. package/src/surfaceColor.test.ts +8 -10
  57. package/src/theme/colorEngine.test.ts +85 -34
  58. package/src/theme/colorEngine.ts +28 -147
  59. package/src/theme/createSurfaceColorDiagnosticCollector.ts +91 -0
  60. package/src/theme/createSurfaceSemanticPolicy.ts +174 -0
  61. package/src/theme/createTheme.ts +2 -1
  62. package/src/theme/defaultTokens.ts +4 -1
  63. package/src/theme/resolveGlobalTokens.ts +4 -1
  64. package/src/theme/resolveRoleSemantics.ts +96 -0
  65. package/src/theme/resolveSurfaceSemanticModel.ts +230 -0
  66. package/src/theme/surfaceColorPolicy.ts +7 -0
  67. package/src/theme/types.ts +65 -1
@@ -1,29 +1,21 @@
1
1
  import type {
2
- ColorSwatch,
3
2
  GeneratedThemeModeColors,
4
3
  GeneratedThemeSwatches,
5
4
  HexColor,
6
5
  SemanticColorReferenceMap,
7
6
  SemanticColorToken,
7
+ ThemeColorMode,
8
8
  } from '@ankhorage/color-theory';
9
9
  import {
10
- createDefaultSemanticStatusSwatches,
10
+ DARK_SEMANTIC_COLOR_REFERENCES,
11
11
  generateThemeModeColors,
12
- getReadableForeground,
12
+ LIGHT_SEMANTIC_COLOR_REFERENCES,
13
13
  parseHexColorOrThrow,
14
14
  } from '@ankhorage/color-theory';
15
15
  import type { ThemeConfig } from '@ankhorage/contracts';
16
16
 
17
- import type {
18
- ActionSemantics,
19
- BorderSemantics,
20
- ContentSemantics,
21
- NeutralSemantics,
22
- RoleSemantics,
23
- SurfaceSemantics,
24
- ThemeSemantics,
25
- ThemeTokens,
26
- } from './types';
17
+ import { resolveSurfaceSemanticModel } from './resolveSurfaceSemanticModel';
18
+ import type { SurfaceColorDiagnostics, ThemeSemantics, ThemeTokens } from './types';
27
19
 
28
20
  /**
29
21
  * Surface semantic resolver: maps color-theory SemanticColorToken references
@@ -46,141 +38,44 @@ export function resolveSemanticColors(
46
38
  ) as SurfaceSemanticColors;
47
39
  }
48
40
 
49
- function buildNeutralSemantics(neutralSwatch: ColorSwatch, isDark: boolean): NeutralSemantics {
50
- if (isDark) {
51
- return {
52
- bg: neutralSwatch[950],
53
- bgSubtle: neutralSwatch[900],
54
- surface: neutralSwatch[900],
55
- surfaceHover: neutralSwatch[800],
56
- surfaceActive: neutralSwatch[700],
57
- border: neutralSwatch[800],
58
- borderStrong: neutralSwatch[600],
59
- divider: neutralSwatch[800],
60
- text: neutralSwatch[50],
61
- textMuted: neutralSwatch[200],
62
- textSubtle: neutralSwatch[300],
63
- };
64
- }
65
- return {
66
- bg: neutralSwatch[50],
67
- bgSubtle: neutralSwatch[100],
68
- surface: neutralSwatch[100],
69
- surfaceHover: neutralSwatch[200],
70
- surfaceActive: neutralSwatch[300],
71
- border: neutralSwatch[200],
72
- borderStrong: neutralSwatch[300],
73
- divider: neutralSwatch[200],
74
- text: neutralSwatch[900],
75
- textMuted: neutralSwatch[700],
76
- textSubtle: neutralSwatch[600],
77
- };
78
- }
79
-
80
- function buildRoleSemantics(swatch: ColorSwatch, isDark: boolean): RoleSemantics {
81
- const base = swatch[500];
82
- const { foreground: onSolidText } = getReadableForeground(base);
83
-
84
- if (isDark) {
85
- return {
86
- base,
87
- hover: swatch[400],
88
- strong: swatch[300],
89
- softBg: swatch[900],
90
- softHover: swatch[800],
91
- softActive: swatch[700],
92
- outline: swatch[500],
93
- onSolidText,
94
- };
95
- }
96
-
97
- return {
98
- base,
99
- hover: swatch[600],
100
- strong: swatch[700],
101
- softBg: swatch[100],
102
- softHover: swatch[200],
103
- softActive: swatch[300],
104
- outline: swatch[400],
105
- onSolidText,
106
- };
107
- }
108
-
109
41
  export function generatePalette(
110
42
  config: ThemeConfig,
111
- mode: 'light' | 'dark' = 'light',
43
+ mode: ThemeColorMode = 'light',
112
44
  ): {
113
45
  colors: ThemeTokens['colors'];
114
46
  swatches: GeneratedThemeSwatches;
115
47
  semantics: ThemeSemantics;
48
+ colorDiagnostics: SurfaceColorDiagnostics;
116
49
  } {
117
50
  const modeConfig = mode === 'dark' ? config.dark : config.light;
118
51
  const isDark = mode === 'dark';
119
52
 
120
- // Throws deterministically on invalid primary color
121
53
  parseHexColorOrThrow(modeConfig.primaryColor);
122
54
 
123
55
  const generated = generateThemeModeColors(modeConfig);
124
56
  const { swatches } = generated;
125
- const neutralSwatch = swatches.neutral;
126
-
127
- const neutral = buildNeutralSemantics(neutralSwatch, isDark);
128
- const brand = buildRoleSemantics(swatches.primary, isDark);
129
-
130
- // Fallback to primary swatch for missing ordinal roles
131
- const secondarySwatch = swatches.secondary ?? swatches.primary;
132
- const tertiarySwatch = swatches.tertiary ?? swatches.primary;
133
- const quaternarySwatch = swatches.quaternary ?? swatches.primary;
134
-
135
- const semanticStatusSwatches = createDefaultSemanticStatusSwatches().swatches;
136
-
137
- const danger = buildRoleSemantics(semanticStatusSwatches.danger, isDark);
138
- const success = buildRoleSemantics(semanticStatusSwatches.success, isDark);
139
- const warning = buildRoleSemantics(semanticStatusSwatches.warning, isDark);
140
- // Keep destructive action and feedback error aligned until a dedicated error status swatch is introduced.
141
- const error = danger;
142
- const info = buildRoleSemantics(secondarySwatch, isDark);
143
- const inverseSurface = isDark ? neutralSwatch[50] : neutralSwatch[900];
144
- const inverseOnSurface = isDark ? neutralSwatch[900] : neutralSwatch[50];
145
-
146
- const surfaceSemantics: SurfaceSemantics = {
147
- default: neutral.surface,
148
- subtle: neutral.bgSubtle,
149
- raised: neutral.surface,
150
- inverse: inverseSurface,
151
- };
152
-
153
- const content: ContentSemantics = {
154
- default: neutral.text,
155
- muted: neutral.textMuted,
156
- subtle: neutral.textSubtle,
157
- inverse: inverseOnSurface,
158
- };
159
-
160
- const border: BorderSemantics = {
161
- default: neutral.border,
162
- strong: neutral.borderStrong,
163
- focus: brand.outline,
164
- };
165
-
166
- const action: ActionSemantics = {
167
- primary: brand,
168
- neutral: buildRoleSemantics(neutralSwatch, isDark),
169
- danger,
170
- };
57
+ const references = isDark ? DARK_SEMANTIC_COLOR_REFERENCES : LIGHT_SEMANTIC_COLOR_REFERENCES;
58
+ const resolved = resolveSemanticColors(generated, references);
59
+ const { semantics, colorDiagnostics } = resolveSurfaceSemanticModel({
60
+ generated,
61
+ mode,
62
+ references,
63
+ resolved,
64
+ });
65
+ const { border, content, error, info, neutral, success, surface, warning } = semantics;
171
66
 
172
67
  const colors: ThemeTokens['colors'] = {
173
- primary: brand.base,
174
- secondary: secondarySwatch[500],
175
- accent: tertiarySwatch[500],
176
- highlight: quaternarySwatch[500],
177
- tertiary: tertiarySwatch[500],
178
- quaternary: quaternarySwatch[500],
68
+ primary: swatches.primary[500],
69
+ secondary: (swatches.secondary ?? swatches.primary)[500],
70
+ accent: (swatches.tertiary ?? swatches.primary)[500],
71
+ highlight: (swatches.quaternary ?? swatches.primary)[500],
72
+ tertiary: (swatches.tertiary ?? swatches.primary)[500],
73
+ quaternary: (swatches.quaternary ?? swatches.primary)[500],
179
74
  background: neutral.bg,
180
- surface: neutral.surface,
181
- text: neutral.text,
182
- textSecondary: neutral.textMuted,
183
- border: neutral.border,
75
+ surface: surface.default,
76
+ text: content.default,
77
+ textSecondary: content.muted,
78
+ border: border.default,
184
79
  error: error.base,
185
80
  success: success.base,
186
81
  warning: warning.base,
@@ -190,21 +85,7 @@ export function generatePalette(
190
85
  return {
191
86
  colors,
192
87
  swatches,
193
- semantics: {
194
- neutral,
195
- brand,
196
- secondary: buildRoleSemantics(secondarySwatch, isDark),
197
- accent: buildRoleSemantics(tertiarySwatch, isDark),
198
- highlight: buildRoleSemantics(quaternarySwatch, isDark),
199
- danger,
200
- success,
201
- warning,
202
- error,
203
- info,
204
- surface: surfaceSemantics,
205
- content,
206
- border,
207
- action,
208
- },
88
+ semantics,
89
+ colorDiagnostics,
209
90
  };
210
91
  }
@@ -0,0 +1,91 @@
1
+ import type {
2
+ ColorContrastContext,
3
+ ColorSelectionTarget,
4
+ ColorSwatch,
5
+ ColorSwatchTiePolicy,
6
+ HexColor,
7
+ } from '@ankhorage/color-theory';
8
+ import {
9
+ getContrastRatio,
10
+ getReadableForeground,
11
+ selectColorSwatchStep,
12
+ } from '@ankhorage/color-theory';
13
+
14
+ import type { SurfaceColorSelectionDiagnostic, SurfaceContrastDiagnostic } from './types';
15
+
16
+ /** Collect deterministic Color Theory evidence while Surface resolves semantic colors. */
17
+ export function createSurfaceColorDiagnosticCollector() {
18
+ return new SurfaceColorDiagnosticCollector();
19
+ }
20
+
21
+ class SurfaceColorDiagnosticCollector {
22
+ readonly selections: SurfaceColorSelectionDiagnostic[] = [];
23
+ readonly contrasts: SurfaceContrastDiagnostic[] = [];
24
+
25
+ readonly measureForeground = (
26
+ id: string,
27
+ foreground: HexColor,
28
+ background: HexColor,
29
+ minimumContrast: number,
30
+ ): HexColor => {
31
+ const contrast = getContrastRatio(foreground, background);
32
+ this.contrasts.push({
33
+ id,
34
+ foreground,
35
+ background,
36
+ contrast,
37
+ minimumContrast,
38
+ passes: contrast >= minimumContrast,
39
+ });
40
+ return foreground;
41
+ };
42
+
43
+ readonly measureSurface = (
44
+ id: string,
45
+ foreground: HexColor,
46
+ background: HexColor,
47
+ minimumContrast: number,
48
+ ): SurfaceContrastDiagnostic => {
49
+ const contrast = getContrastRatio(foreground, background);
50
+ return {
51
+ id,
52
+ foreground,
53
+ background,
54
+ contrast,
55
+ minimumContrast,
56
+ passes: contrast >= minimumContrast,
57
+ };
58
+ };
59
+
60
+ readonly selectColor = (
61
+ id: string,
62
+ swatch: ColorSwatch,
63
+ target: ColorSelectionTarget,
64
+ contexts: readonly ColorContrastContext[],
65
+ tiePolicy: ColorSwatchTiePolicy,
66
+ ): HexColor => {
67
+ const result = selectColorSwatchStep(swatch, target, contexts, tiePolicy);
68
+ this.selections.push({ id, result });
69
+ if (!result.selected) {
70
+ throw new Error(`[surface] No '${id}' swatch step satisfies the required contrast.`);
71
+ }
72
+ return result.selected.hex;
73
+ };
74
+
75
+ readonly selectForeground = (
76
+ id: string,
77
+ background: HexColor,
78
+ minimumContrast: number,
79
+ ): HexColor => {
80
+ const selected = getReadableForeground(background);
81
+ this.contrasts.push({
82
+ id,
83
+ foreground: selected.foreground,
84
+ background,
85
+ contrast: selected.contrast,
86
+ minimumContrast,
87
+ passes: selected.contrast >= minimumContrast,
88
+ });
89
+ return selected.foreground;
90
+ };
91
+ }
@@ -0,0 +1,174 @@
1
+ import type {
2
+ ColorContrastContext,
3
+ ColorSelectionTarget,
4
+ ColorSwatch,
5
+ ColorSwatchTiePolicy,
6
+ HexColor,
7
+ SemanticColorToken,
8
+ } from '@ankhorage/color-theory';
9
+ import { parseHexColorOrThrow } from '@ankhorage/color-theory';
10
+
11
+ import { SURFACE_COLOR_POLICY } from './surfaceColorPolicy';
12
+ import type {
13
+ ContentSemantics,
14
+ NeutralSemantics,
15
+ SurfaceContrastDiagnostic,
16
+ SurfaceSemantics,
17
+ } from './types';
18
+
19
+ type ResolvedSemanticColors = Record<SemanticColorToken, HexColor>;
20
+
21
+ interface SurfaceSemanticMeasurements {
22
+ measureForeground: (
23
+ id: string,
24
+ foreground: HexColor,
25
+ background: HexColor,
26
+ minimumContrast: number,
27
+ ) => HexColor;
28
+ measureSurface: (
29
+ id: string,
30
+ foreground: HexColor,
31
+ background: HexColor,
32
+ minimumContrast: number,
33
+ ) => SurfaceContrastDiagnostic;
34
+ }
35
+
36
+ /** Create the canonical Surface policy for selecting and measuring semantic colors. */
37
+ export function createSurfaceSemanticPolicy(
38
+ resolved: ResolvedSemanticColors,
39
+ isDark: boolean,
40
+ measurements: SurfaceSemanticMeasurements,
41
+ ) {
42
+ return new SurfaceSemanticPolicy(resolved, isDark, measurements);
43
+ }
44
+
45
+ class SurfaceSemanticPolicy {
46
+ readonly adjacentSurfaceContexts: readonly ColorContrastContext[];
47
+ readonly textSurfaceContexts: readonly ColorContrastContext[];
48
+ readonly tiePolicy: ColorSwatchTiePolicy;
49
+
50
+ constructor(
51
+ private readonly resolved: ResolvedSemanticColors,
52
+ private readonly isDark: boolean,
53
+ private readonly measurements: SurfaceSemanticMeasurements,
54
+ ) {
55
+ this.adjacentSurfaceContexts = this.createSurfaceContexts(SURFACE_COLOR_POLICY.uiContrast);
56
+ this.textSurfaceContexts = this.createSurfaceContexts(SURFACE_COLOR_POLICY.textContrast);
57
+ this.tiePolicy = isDark ? 'lower-step' : 'higher-step';
58
+ }
59
+
60
+ buildNeutral(neutralSwatch: ColorSwatch): NeutralSemantics {
61
+ return {
62
+ bg: this.resolved.background,
63
+ bgSubtle: this.resolved.surface,
64
+ surface: this.resolved.surface,
65
+ surfaceHover: this.isDark ? neutralSwatch[800] : neutralSwatch[200],
66
+ surfaceActive: this.isDark ? neutralSwatch[700] : neutralSwatch[300],
67
+ border: this.resolved.border,
68
+ borderStrong: this.isDark ? neutralSwatch[600] : neutralSwatch[300],
69
+ divider: this.resolved.divider,
70
+ text: this.resolved.text,
71
+ textMuted: this.resolved.textMuted,
72
+ textSubtle: this.isDark ? neutralSwatch[300] : neutralSwatch[600],
73
+ disabledBg: this.resolved.disabledBg,
74
+ disabledText: this.resolved.disabledText,
75
+ };
76
+ }
77
+
78
+ buildSurface(inverse: HexColor): SurfaceSemantics {
79
+ return {
80
+ default: this.resolved.surface,
81
+ subtle: this.resolved.background,
82
+ raised: this.resolved.surfaceRaised,
83
+ sunken: this.resolved.background,
84
+ overlay: this.resolved.surfaceRaised,
85
+ scrim: '#0000008f',
86
+ disabled: this.resolved.disabledBg,
87
+ inverse,
88
+ };
89
+ }
90
+
91
+ measureContent(content: ContentSemantics, surface: SurfaceSemantics): void {
92
+ const pairs = [
93
+ [
94
+ 'content.default/background',
95
+ content.default,
96
+ surface.sunken,
97
+ SURFACE_COLOR_POLICY.textContrast,
98
+ ],
99
+ [
100
+ 'content.default/surface',
101
+ content.default,
102
+ surface.default,
103
+ SURFACE_COLOR_POLICY.textContrast,
104
+ ],
105
+ ['content.muted/surface', content.muted, surface.default, SURFACE_COLOR_POLICY.textContrast],
106
+ [
107
+ 'content.subtle/surface',
108
+ content.subtle,
109
+ surface.default,
110
+ SURFACE_COLOR_POLICY.textContrast,
111
+ ],
112
+ [
113
+ 'content.disabled/disabled',
114
+ content.disabled,
115
+ surface.disabled,
116
+ SURFACE_COLOR_POLICY.disabledContrast,
117
+ ],
118
+ [
119
+ 'content.inverse/inverse',
120
+ content.inverse,
121
+ surface.inverse,
122
+ SURFACE_COLOR_POLICY.textContrast,
123
+ ],
124
+ ] as const;
125
+ for (const [id, foreground, background, minimumContrast] of pairs) {
126
+ this.measurements.measureForeground(
127
+ id,
128
+ parseHexColorOrThrow(foreground),
129
+ parseHexColorOrThrow(background),
130
+ minimumContrast,
131
+ );
132
+ }
133
+ }
134
+
135
+ measureSurfaceSeparation(surface: SurfaceSemantics): SurfaceContrastDiagnostic[] {
136
+ return [
137
+ this.measureSurface('surface.default', surface.default),
138
+ this.measureSurface('surface.raised', surface.raised),
139
+ this.measureSurface('surface.disabled', surface.disabled),
140
+ ];
141
+ }
142
+
143
+ neutralTarget(): ColorSelectionTarget {
144
+ return { lightness: this.isDark ? 0.65 : 0.55, chroma: 0.02 };
145
+ }
146
+
147
+ roleTarget(): ColorSelectionTarget {
148
+ return { lightness: this.isDark ? 0.65 : 0.5, chroma: 0.15 };
149
+ }
150
+
151
+ strongNeutralTarget(): ColorSelectionTarget {
152
+ return { lightness: this.isDark ? 0.8 : 0.4, chroma: 0.02 };
153
+ }
154
+
155
+ textTarget(): ColorSelectionTarget {
156
+ return { lightness: this.isDark ? 0.75 : 0.45, chroma: 0.15 };
157
+ }
158
+
159
+ private createSurfaceContexts(minimumContrast: number): readonly ColorContrastContext[] {
160
+ return [
161
+ { id: 'background', against: this.resolved.background, minimumContrast },
162
+ { id: 'surface', against: this.resolved.surface, minimumContrast },
163
+ ];
164
+ }
165
+
166
+ private measureSurface(id: string, foreground: string): SurfaceContrastDiagnostic {
167
+ return this.measurements.measureSurface(
168
+ id,
169
+ parseHexColorOrThrow(foreground),
170
+ this.resolved.background,
171
+ SURFACE_COLOR_POLICY.surfaceSeparation,
172
+ );
173
+ }
174
+ }
@@ -28,12 +28,13 @@ export function createTheme(
28
28
  mode: 'light' | 'dark' = 'light',
29
29
  activeFontId?: string | null,
30
30
  ): SurfaceTheme {
31
- const { colors, swatches, semantics } = generatePalette(config, mode);
31
+ const { colors, swatches, semantics, colorDiagnostics } = generatePalette(config, mode);
32
32
  const theme: SurfaceTheme = {
33
33
  ...resolveGlobalTokens(config.tokens),
34
34
  colors,
35
35
  swatches,
36
36
  semantics,
37
+ colorDiagnostics,
37
38
  config,
38
39
  };
39
40
 
@@ -14,7 +14,10 @@ const EMPTY_FONT_MAP: Record<FontWeight, string | undefined> = {
14
14
  normal: undefined,
15
15
  };
16
16
 
17
- export const DEFAULT_TOKENS: Omit<ThemeTokens, 'colors' | 'swatches' | 'semantics'> = {
17
+ export const DEFAULT_TOKENS: Omit<
18
+ ThemeTokens,
19
+ 'colors' | 'swatches' | 'semantics' | 'colorDiagnostics'
20
+ > = {
18
21
  spacing: { none: 0, xs: 4, s: 8, m: 16, l: 24, xl: 32, xxl: 48 },
19
22
  radii: { none: 0, s: 4, m: 8, l: 16, full: 9999 },
20
23
  typography: {
@@ -6,7 +6,10 @@ import { resolveNumericTokenOverrides } from './resolveNumericTokenOverrides';
6
6
  import { resolveWeightTokenOverrides } from './resolveWeightTokenOverrides';
7
7
  import type { ThemeTokens } from './types';
8
8
 
9
- type GlobalThemeTokens = Omit<ThemeTokens, 'colors' | 'swatches' | 'semantics'>;
9
+ type GlobalThemeTokens = Omit<
10
+ ThemeTokens,
11
+ 'colors' | 'swatches' | 'semantics' | 'colorDiagnostics'
12
+ >;
10
13
 
11
14
  export function resolveGlobalTokens(overrides?: ThemeGlobalTokenOverrides): GlobalThemeTokens {
12
15
  validateZeroTokens(overrides);
@@ -0,0 +1,96 @@
1
+ import type { ColorSwatch, HexColor } from '@ankhorage/color-theory';
2
+
3
+ import { SURFACE_COLOR_POLICY } from './surfaceColorPolicy';
4
+ import type { RoleSemantics } from './types';
5
+
6
+ interface ResolveRoleSemanticsInput {
7
+ base?: HexColor;
8
+ disabledBg: HexColor;
9
+ disabledText: HexColor;
10
+ hover?: HexColor;
11
+ id: string;
12
+ isDark: boolean;
13
+ measureForeground: (
14
+ id: string,
15
+ foreground: HexColor,
16
+ background: HexColor,
17
+ minimumContrast: number,
18
+ ) => HexColor;
19
+ onSurfaceText: HexColor;
20
+ outline: HexColor;
21
+ selectForeground: (id: string, background: HexColor, minimumContrast: number) => HexColor;
22
+ swatch: ColorSwatch;
23
+ }
24
+
25
+ interface RoleStateColors {
26
+ base: HexColor;
27
+ hover: HexColor;
28
+ softActive: HexColor;
29
+ softBg: HexColor;
30
+ softHover: HexColor;
31
+ strong: HexColor;
32
+ }
33
+
34
+ /** Resolve every filled role state together with its measured foreground. */
35
+ export function resolveRoleSemantics({
36
+ base,
37
+ disabledBg,
38
+ disabledText,
39
+ hover,
40
+ id,
41
+ isDark,
42
+ measureForeground,
43
+ onSurfaceText,
44
+ outline,
45
+ selectForeground,
46
+ swatch,
47
+ }: ResolveRoleSemanticsInput): RoleSemantics {
48
+ const states = resolveRoleStateColors(swatch, isDark, base, hover);
49
+
50
+ return {
51
+ ...states,
52
+ outline,
53
+ onSurfaceText,
54
+ onSolidText: selectForeground(`${id}.base`, states.base, SURFACE_COLOR_POLICY.textContrast),
55
+ onHoverText: selectForeground(`${id}.hover`, states.hover, SURFACE_COLOR_POLICY.textContrast),
56
+ onStrongText: selectForeground(
57
+ `${id}.strong`,
58
+ states.strong,
59
+ SURFACE_COLOR_POLICY.textContrast,
60
+ ),
61
+ onSoftText: selectForeground(`${id}.soft`, states.softBg, SURFACE_COLOR_POLICY.textContrast),
62
+ onSoftHoverText: selectForeground(
63
+ `${id}.softHover`,
64
+ states.softHover,
65
+ SURFACE_COLOR_POLICY.textContrast,
66
+ ),
67
+ onSoftActiveText: selectForeground(
68
+ `${id}.softActive`,
69
+ states.softActive,
70
+ SURFACE_COLOR_POLICY.textContrast,
71
+ ),
72
+ disabledBg,
73
+ onDisabledText: measureForeground(
74
+ `${id}.disabled`,
75
+ disabledText,
76
+ disabledBg,
77
+ SURFACE_COLOR_POLICY.disabledContrast,
78
+ ),
79
+ };
80
+ }
81
+
82
+ function resolveRoleStateColors(
83
+ swatch: ColorSwatch,
84
+ isDark: boolean,
85
+ base?: HexColor,
86
+ hover?: HexColor,
87
+ ): RoleStateColors {
88
+ return {
89
+ base: base ?? swatch[500],
90
+ hover: hover ?? (isDark ? swatch[400] : swatch[600]),
91
+ strong: isDark ? swatch[300] : swatch[700],
92
+ softBg: isDark ? swatch[900] : swatch[100],
93
+ softHover: isDark ? swatch[800] : swatch[200],
94
+ softActive: isDark ? swatch[700] : swatch[300],
95
+ };
96
+ }