@wordpress/theme 0.1.0 → 0.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 (168) hide show
  1. package/README.md +69 -9
  2. package/bin/generate-default-ramps/index.ts +49 -0
  3. package/bin/generate-primitive-tokens/index.ts +14 -9
  4. package/bin/terrazzo-plugin-ds-tokens-docs/index.ts +5 -24
  5. package/bin/terrazzo-plugin-figma-ds-token-manager/index.ts +11 -4
  6. package/bin/terrazzo-plugin-inline-alias-values/index.ts +84 -0
  7. package/bin/terrazzo-plugin-known-wpds-css-variables/index.ts +19 -39
  8. package/build/color-ramps/index.js +21 -39
  9. package/build/color-ramps/index.js.map +3 -3
  10. package/build/color-ramps/lib/color-utils.js +39 -0
  11. package/build/color-ramps/lib/color-utils.js.map +7 -0
  12. package/build/color-ramps/lib/constants.js +20 -27
  13. package/build/color-ramps/lib/constants.js.map +3 -3
  14. package/build/color-ramps/lib/default-ramps.js +220 -0
  15. package/build/color-ramps/lib/default-ramps.js.map +7 -0
  16. package/build/color-ramps/lib/find-color-with-constraints.js +60 -91
  17. package/build/color-ramps/lib/find-color-with-constraints.js.map +3 -3
  18. package/build/color-ramps/lib/index.js +77 -119
  19. package/build/color-ramps/lib/index.js.map +3 -3
  20. package/build/color-ramps/lib/ramp-configs.js +15 -14
  21. package/build/color-ramps/lib/ramp-configs.js.map +2 -2
  22. package/build/color-ramps/lib/register-color-spaces.js +7 -0
  23. package/build/color-ramps/lib/register-color-spaces.js.map +7 -0
  24. package/build/color-ramps/lib/taper-chroma.js +35 -27
  25. package/build/color-ramps/lib/taper-chroma.js.map +3 -3
  26. package/build/color-ramps/lib/types.js +2 -1
  27. package/build/color-ramps/lib/types.js.map +1 -1
  28. package/build/color-ramps/lib/utils.js +75 -11
  29. package/build/color-ramps/lib/utils.js.map +2 -2
  30. package/build/context.js +3 -2
  31. package/build/context.js.map +1 -1
  32. package/build/index.js +2 -1
  33. package/build/index.js.map +1 -1
  34. package/build/lock-unlock.js +3 -2
  35. package/build/lock-unlock.js.map +1 -1
  36. package/build/prebuilt/js/design-tokens.js +19 -11
  37. package/build/prebuilt/js/design-tokens.js.map +2 -2
  38. package/build/prebuilt/json/figma.json +165 -783
  39. package/build/prebuilt/ts/color-tokens.js +137 -0
  40. package/build/prebuilt/ts/color-tokens.js.map +7 -0
  41. package/build/private-apis.js +3 -2
  42. package/build/private-apis.js.map +1 -1
  43. package/build/theme-provider.js +19 -17
  44. package/build/theme-provider.js.map +4 -4
  45. package/build/token-id.js +30 -0
  46. package/build/token-id.js.map +7 -0
  47. package/build/types/css-modules.d.js +0 -1
  48. package/build/types.js +2 -1
  49. package/build/types.js.map +1 -1
  50. package/build/use-theme-provider-styles.js +67 -62
  51. package/build/use-theme-provider-styles.js.map +3 -3
  52. package/build-module/color-ramps/index.js +20 -28
  53. package/build-module/color-ramps/index.js.map +2 -2
  54. package/build-module/color-ramps/lib/color-utils.js +19 -0
  55. package/build-module/color-ramps/lib/color-utils.js.map +7 -0
  56. package/build-module/color-ramps/lib/constants.js +14 -11
  57. package/build-module/color-ramps/lib/constants.js.map +2 -2
  58. package/build-module/color-ramps/lib/default-ramps.js +196 -0
  59. package/build-module/color-ramps/lib/default-ramps.js.map +7 -0
  60. package/build-module/color-ramps/lib/find-color-with-constraints.js +61 -87
  61. package/build-module/color-ramps/lib/find-color-with-constraints.js.map +2 -2
  62. package/build-module/color-ramps/lib/index.js +85 -109
  63. package/build-module/color-ramps/lib/index.js.map +3 -3
  64. package/build-module/color-ramps/lib/ramp-configs.js +14 -13
  65. package/build-module/color-ramps/lib/ramp-configs.js.map +2 -2
  66. package/build-module/color-ramps/lib/register-color-spaces.js +7 -0
  67. package/build-module/color-ramps/lib/register-color-spaces.js.map +7 -0
  68. package/build-module/color-ramps/lib/taper-chroma.js +40 -16
  69. package/build-module/color-ramps/lib/taper-chroma.js.map +2 -2
  70. package/build-module/color-ramps/lib/utils.js +70 -6
  71. package/build-module/color-ramps/lib/utils.js.map +2 -2
  72. package/build-module/context.js +2 -1
  73. package/build-module/context.js.map +1 -1
  74. package/build-module/index.js +1 -0
  75. package/build-module/index.js.map +1 -1
  76. package/build-module/lock-unlock.js +2 -1
  77. package/build-module/lock-unlock.js.map +1 -1
  78. package/build-module/prebuilt/js/design-tokens.js +18 -10
  79. package/build-module/prebuilt/js/design-tokens.js.map +2 -2
  80. package/build-module/prebuilt/json/figma.json +165 -783
  81. package/build-module/prebuilt/ts/color-tokens.js +117 -0
  82. package/build-module/prebuilt/ts/color-tokens.js.map +7 -0
  83. package/build-module/private-apis.js +2 -1
  84. package/build-module/private-apis.js.map +1 -1
  85. package/build-module/theme-provider.js +18 -6
  86. package/build-module/theme-provider.js.map +3 -3
  87. package/build-module/token-id.js +6 -0
  88. package/build-module/token-id.js.map +7 -0
  89. package/build-module/use-theme-provider-styles.js +69 -57
  90. package/build-module/use-theme-provider-styles.js.map +2 -2
  91. package/build-types/color-ramps/index.d.ts +9 -16
  92. package/build-types/color-ramps/index.d.ts.map +1 -1
  93. package/build-types/color-ramps/lib/color-utils.d.ts +22 -0
  94. package/build-types/color-ramps/lib/color-utils.d.ts.map +1 -0
  95. package/build-types/color-ramps/lib/constants.d.ts +7 -9
  96. package/build-types/color-ramps/lib/constants.d.ts.map +1 -1
  97. package/build-types/color-ramps/lib/default-ramps.d.ts +7 -0
  98. package/build-types/color-ramps/lib/default-ramps.d.ts.map +1 -0
  99. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts +8 -7
  100. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts.map +1 -1
  101. package/build-types/color-ramps/lib/index.d.ts +5 -2
  102. package/build-types/color-ramps/lib/index.d.ts.map +1 -1
  103. package/build-types/color-ramps/lib/register-color-spaces.d.ts +2 -0
  104. package/build-types/color-ramps/lib/register-color-spaces.d.ts.map +1 -0
  105. package/build-types/color-ramps/lib/taper-chroma.d.ts +7 -3
  106. package/build-types/color-ramps/lib/taper-chroma.d.ts.map +1 -1
  107. package/build-types/color-ramps/lib/utils.d.ts +28 -5
  108. package/build-types/color-ramps/lib/utils.d.ts.map +1 -1
  109. package/build-types/color-ramps/stories/index.story.d.ts.map +1 -1
  110. package/build-types/prebuilt/ts/color-tokens.d.ts +7 -0
  111. package/build-types/prebuilt/ts/color-tokens.d.ts.map +1 -0
  112. package/build-types/stories/index.story.d.ts.map +1 -1
  113. package/build-types/theme-provider.d.ts.map +1 -1
  114. package/build-types/token-id.d.ts +9 -0
  115. package/build-types/token-id.d.ts.map +1 -0
  116. package/build-types/use-theme-provider-styles.d.ts +4 -0
  117. package/build-types/use-theme-provider-styles.d.ts.map +1 -1
  118. package/docs/ds-tokens.md +22 -156
  119. package/package.json +19 -9
  120. package/src/color-ramps/index.ts +24 -41
  121. package/src/color-ramps/lib/color-utils.ts +34 -0
  122. package/src/color-ramps/lib/constants.ts +13 -9
  123. package/src/color-ramps/lib/default-ramps.ts +200 -0
  124. package/src/color-ramps/lib/find-color-with-constraints.ts +83 -116
  125. package/src/color-ramps/lib/index.ts +107 -145
  126. package/src/color-ramps/lib/ramp-configs.ts +3 -3
  127. package/src/color-ramps/lib/register-color-spaces.ts +13 -0
  128. package/src/color-ramps/lib/taper-chroma.ts +47 -19
  129. package/src/color-ramps/lib/utils.ts +117 -14
  130. package/src/color-ramps/stories/index.story.tsx +16 -22
  131. package/src/color-ramps/test/__snapshots__/index.test.ts.snap +45722 -376
  132. package/src/color-ramps/test/index.test.ts +68 -29
  133. package/src/prebuilt/css/design-tokens.css +88 -355
  134. package/src/prebuilt/js/design-tokens.js +17 -10
  135. package/src/prebuilt/json/figma.json +165 -783
  136. package/src/prebuilt/ts/color-tokens.ts +117 -0
  137. package/src/stories/index.story.tsx +4 -18
  138. package/src/test/token-id.test.ts +12 -0
  139. package/src/token-id.ts +9 -0
  140. package/src/use-theme-provider-styles.ts +67 -60
  141. package/terrazzo.config.ts +15 -12
  142. package/tokens/color.json +221 -69
  143. package/tokens/dimension.json +75 -0
  144. package/tsconfig.bin.json +13 -0
  145. package/tsconfig.bin.tsbuildinfo +1 -0
  146. package/tsconfig.json +6 -4
  147. package/tsconfig.src.json +9 -0
  148. package/tsconfig.src.tsbuildinfo +1 -0
  149. package/bin/build-tokens.js +0 -83
  150. package/build/color-ramps/lib/cache-utils.js +0 -57
  151. package/build/color-ramps/lib/cache-utils.js.map +0 -7
  152. package/build/prebuilt/ts/design-tokens.js +0 -354
  153. package/build/prebuilt/ts/design-tokens.js.map +0 -7
  154. package/build/style.module.css.js +0 -2
  155. package/build-module/color-ramps/lib/cache-utils.js +0 -31
  156. package/build-module/color-ramps/lib/cache-utils.js.map +0 -7
  157. package/build-module/prebuilt/ts/design-tokens.js +0 -334
  158. package/build-module/prebuilt/ts/design-tokens.js.map +0 -7
  159. package/build-module/style.module.css.js +0 -1
  160. package/build-style/style.css +0 -3
  161. package/build-types/color-ramps/lib/cache-utils.d.ts +0 -22
  162. package/build-types/color-ramps/lib/cache-utils.d.ts.map +0 -1
  163. package/build-types/prebuilt/ts/design-tokens.d.ts +0 -7
  164. package/build-types/prebuilt/ts/design-tokens.d.ts.map +0 -1
  165. package/src/color-ramps/lib/cache-utils.ts +0 -56
  166. package/src/prebuilt/ts/design-tokens.ts +0 -335
  167. package/tokens/spacing.json +0 -45
  168. package/tsconfig.tsbuildinfo +0 -1
@@ -1,21 +1,28 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import Color from 'colorjs.io';
4
+ import { get, OKLCH, type ColorTypes } from 'colorjs.io/fn';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import { clampToGamut } from './utils';
10
- import {
11
- WHITE,
12
- BLACK,
13
- LIGHTNESS_EPSILON,
14
- MAX_BISECTION_ITERATIONS,
15
- } from './constants';
16
- import { getCachedContrast } from './cache-utils';
9
+ import './register-color-spaces';
10
+ import { clampToGamut, solveWithBisect } from './utils';
11
+ import { WHITE, BLACK, CONTRAST_EPSILON } from './constants';
12
+ import { getContrast } from './color-utils';
17
13
  import { type TaperChromaOptions, taperChroma } from './taper-chroma';
18
14
 
15
+ /**
16
+ * Difference of contrast values that grows linearly with the Y luminance.
17
+ * We get more precise linear interpolations when we use this.
18
+ * @param c1 First contrast.
19
+ * @param c2 Second contrast.
20
+ * @return Difference of logarithms.
21
+ */
22
+ function cdiff( c1: number, c2: number ) {
23
+ return Math.log( c1 / c2 );
24
+ }
25
+
19
26
  /**
20
27
  * Solve for L such that:
21
28
  * - the L applied to the seed meets the contrast target against the reference
@@ -27,115 +34,98 @@ import { type TaperChromaOptions, taperChroma } from './taper-chroma';
27
34
  * @param target
28
35
  * @param direction
29
36
  * @param options
30
- * @param options.strict
31
- * @param options.debug
32
37
  * @param options.lightnessConstraint
33
38
  * @param options.lightnessConstraint.type
34
39
  * @param options.lightnessConstraint.value
35
40
  * @param options.taperChromaOptions
36
41
  */
37
42
  export function findColorMeetingRequirements(
38
- reference: Color,
39
- seed: Color,
43
+ reference: ColorTypes,
44
+ seed: ColorTypes,
40
45
  target: number,
41
46
  direction: 'lighter' | 'darker',
42
47
  {
43
48
  lightnessConstraint,
44
49
  taperChromaOptions,
45
- strict = true,
46
- debug = false,
47
50
  }: {
48
51
  lightnessConstraint?: {
49
52
  type: 'force' | 'onlyIfSucceeds';
50
53
  value: number;
51
54
  };
52
55
  taperChromaOptions?: TaperChromaOptions;
53
- strict?: boolean;
54
- debug?: boolean;
55
56
  } = {}
56
- ): { color: Color; reached: boolean; achieved: number } {
57
+ ): { color: ColorTypes; reached: boolean; achieved: number; deficit?: number } {
57
58
  // A target of 1 means same color.
58
59
  // A target lower than 1 doesn't make sense.
59
60
  if ( target <= 1 ) {
60
- return { color: seed.clone(), reached: true, achieved: 1 };
61
+ return {
62
+ color: reference,
63
+ reached: true,
64
+ achieved: 1,
65
+ };
61
66
  }
62
67
 
63
- if ( lightnessConstraint ) {
64
- // Apply a specific L value.
65
- // Useful when pinning a step to a specific lightness, of to specify
66
- // min/max L values.
67
- let newL = lightnessConstraint.value;
68
- let newC = seed.oklch.c;
68
+ function getColorForL( l: number ): ColorTypes {
69
+ let newL = l;
70
+ let newC = get( seed, [ OKLCH, 'c' ] );
69
71
 
70
72
  if ( taperChromaOptions ) {
71
- ( { l: newL, c: newC } = taperChroma(
72
- seed,
73
- newL,
74
- taperChromaOptions
75
- ) );
73
+ const tapered = taperChroma( seed, newL, taperChromaOptions );
74
+ // taperChroma returns either { l, c } or a ColorObject
75
+ if ( 'l' in tapered && 'c' in tapered ) {
76
+ newL = tapered.l;
77
+ newC = tapered.c;
78
+ } else {
79
+ // It's already a ColorObject, return it directly
80
+ return tapered;
81
+ }
76
82
  }
77
83
 
78
- const colorWithExactL = clampToGamut(
79
- new Color( 'oklch', [ newL, newC, seed.oklch.h ] )
80
- );
81
- const exactLContrast = getCachedContrast( reference, colorWithExactL );
84
+ return clampToGamut( {
85
+ spaceId: 'oklch',
86
+ coords: [ newL, newC, get( seed, [ OKLCH, 'h' ] ) ],
87
+ } );
88
+ }
82
89
 
83
- if ( debug ) {
84
- // eslint-disable-next-line no-console
85
- console.log(
86
- `Succeeded with ${ lightnessConstraint.type } lightness`,
87
- lightnessConstraint.value,
88
- colorWithExactL.oklch.l
89
- );
90
- }
90
+ // Set the boundary based on the direction.
91
+ const mostContrastingL = direction === 'lighter' ? 1 : 0;
92
+ const mostContrastingColor = direction === 'lighter' ? WHITE : BLACK;
93
+ const highestContrast = getContrast( reference, mostContrastingColor );
94
+
95
+ if ( lightnessConstraint ) {
96
+ // Apply a specific L value.
97
+ // Useful when pinning a step to a specific lightness, of to specify
98
+ // min/max L values.
99
+ const colorWithExactL = getColorForL( lightnessConstraint.value );
100
+ const exactLContrast = getContrast( reference, colorWithExactL );
101
+ const exactLContrastMeetsTarget =
102
+ cdiff( exactLContrast, target ) >= -CONTRAST_EPSILON;
91
103
 
92
104
  // If the L constraint is of "force" type, apply it even when it doesn't
93
105
  // meet the contrast target.
94
106
  if (
95
- lightnessConstraint.type === 'force' ||
96
- exactLContrast >= target
107
+ exactLContrastMeetsTarget ||
108
+ lightnessConstraint.type === 'force'
97
109
  ) {
98
110
  return {
99
111
  color: colorWithExactL,
100
- reached: exactLContrast >= target,
112
+ reached: exactLContrastMeetsTarget,
101
113
  achieved: exactLContrast,
114
+ deficit: exactLContrastMeetsTarget
115
+ ? cdiff( exactLContrast, highestContrast )
116
+ : cdiff( target, exactLContrast ),
102
117
  };
103
118
  }
104
119
  }
105
120
 
106
- // Set the boundary based on the direction.
107
- const mostContrastingL = direction === 'lighter' ? 1 : 0;
108
- const mostContrastingColor = direction === 'lighter' ? WHITE : BLACK;
109
- const highestPossibleContrast = getCachedContrast(
110
- reference,
111
- mostContrastingColor
112
- );
113
-
114
- // If even the most contrasting color can't reach the target,
115
- // the target is unreachable.
116
- if ( highestPossibleContrast < target ) {
117
- if ( strict ) {
118
- throw new Error(
119
- `Contrast target ${ target.toFixed(
120
- 2
121
- ) }:1 unreachable in ${ direction } direction against ${ mostContrastingColor.toString() }` +
122
- `(boundary achieves ${ highestPossibleContrast.toFixed(
123
- 3
124
- ) }:1).`
125
- );
126
- }
127
-
128
- if ( debug ) {
129
- // eslint-disable-next-line no-console
130
- console.log(
131
- 'Did not succeeded because it reached the limit',
132
- mostContrastingL
133
- );
134
- }
121
+ // If even the most contrasting color can't reach the target, the target is unreachable.
122
+ // On the other hand, if the contrast is very close to the target, we consider it reached.
123
+ if ( cdiff( highestContrast, target ) <= CONTRAST_EPSILON ) {
135
124
  return {
136
125
  color: mostContrastingColor,
137
- reached: false,
138
- achieved: highestPossibleContrast,
126
+ reached: cdiff( highestContrast, target ) >= -CONTRAST_EPSILON,
127
+ achieved: highestContrast,
128
+ deficit: cdiff( target, highestContrast ),
139
129
  };
140
130
  }
141
131
 
@@ -143,48 +133,25 @@ export function findColorMeetingRequirements(
143
133
  // Originally this was seed.oklch.l — although it's an assumption that works
144
134
  // only when we know for sure the direction of the search.
145
135
  // TODO: can we bring this back to seed.oklch.l ?
146
- let worseL = reference.oklch.l;
147
- let betterL = mostContrastingL;
148
-
149
- let bestContrastFound = highestPossibleContrast;
150
- let resultingColor = mostContrastingColor;
151
-
152
- for (
153
- let i = 0;
154
- i < MAX_BISECTION_ITERATIONS &&
155
- Math.abs( betterL - worseL ) > LIGHTNESS_EPSILON;
156
- i++
157
- ) {
158
- let newL = ( worseL + betterL ) / 2;
159
- let newC = seed.oklch.c;
160
-
161
- if ( taperChromaOptions ) {
162
- ( { l: newL, c: newC } = taperChroma(
163
- seed,
164
- newL,
165
- taperChromaOptions
166
- ) );
167
- }
168
-
169
- const newColor = clampToGamut(
170
- new Color( 'oklch', [ newL, newC, seed.oklch.h ] )
171
- );
172
- const newContrast = getCachedContrast( reference, newColor );
173
-
174
- if ( newContrast >= target ) {
175
- betterL = newL;
176
- // Only update the resulting color when the target is met, this ensuring
177
- // at the end of the search the target is always met.
178
- bestContrastFound = newContrast;
179
- resultingColor = newColor;
180
- } else {
181
- worseL = newL;
182
- }
183
- }
136
+ const lowerL = get( reference, [ OKLCH, 'l' ] );
137
+ const lowerContrast = cdiff( 1, target );
138
+ const upperL = mostContrastingL;
139
+ const upperContrast = cdiff( highestContrast, target );
140
+
141
+ const bestColor = solveWithBisect(
142
+ getColorForL,
143
+ ( c: ColorTypes ) => cdiff( getContrast( reference, c ), target ),
144
+ lowerL,
145
+ lowerContrast,
146
+ upperL,
147
+ upperContrast
148
+ );
184
149
 
185
150
  return {
186
- color: resultingColor,
151
+ color: bestColor,
187
152
  reached: true,
188
- achieved: bestContrastFound,
153
+ achieved: target,
154
+ // Negative number that specifies how much room we have.
155
+ deficit: cdiff( target, highestContrast ),
189
156
  };
190
157
  }
@@ -1,18 +1,29 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import Color from 'colorjs.io';
4
+ import {
5
+ clone,
6
+ get,
7
+ OKLCH,
8
+ parse,
9
+ set,
10
+ type ColorTypes,
11
+ type PlainColorObject,
12
+ } from 'colorjs.io/fn';
5
13
 
6
14
  /**
7
15
  * Internal dependencies
8
16
  */
9
- import { getCachedContrast, getColorString } from './cache-utils';
17
+ import './register-color-spaces';
18
+ import { getContrast, getColorString } from './color-utils';
10
19
  import { findColorMeetingRequirements } from './find-color-with-constraints';
11
20
  import {
12
21
  clampToGamut,
13
22
  sortByDependency,
14
23
  computeBetterFgColorDirection,
15
24
  adjustContrastTarget,
25
+ stepsForStep,
26
+ solveWithBisect,
16
27
  } from './utils';
17
28
 
18
29
  import type {
@@ -22,7 +33,7 @@ import type {
22
33
  RampConfig,
23
34
  RampResult,
24
35
  } from './types';
25
- import { LIGHTNESS_EPSILON, MAX_BISECTION_ITERATIONS } from './constants';
36
+ import { CONTRAST_EPSILON } from './constants';
26
37
 
27
38
  /**
28
39
  * Calculate a complete color ramp based on the provided configuration.
@@ -36,7 +47,6 @@ import { LIGHTNESS_EPSILON, MAX_BISECTION_ITERATIONS } from './constants';
36
47
  * @param params.pinLightness - Optional lightness override for a given step
37
48
  * @param params.pinLightness.stepName
38
49
  * @param params.pinLightness.value
39
- * @param params.debug
40
50
  * @return Object containing ramp results and satisfaction status
41
51
  */
42
52
  function calculateRamp( {
@@ -46,9 +56,8 @@ function calculateRamp( {
46
56
  mainDir,
47
57
  oppDir,
48
58
  pinLightness,
49
- debug = false,
50
59
  }: {
51
- seed: Color;
60
+ seed: ColorTypes;
52
61
  sortedSteps: ( keyof Ramp )[];
53
62
  config: RampConfig;
54
63
  mainDir: RampDirection;
@@ -57,19 +66,18 @@ function calculateRamp( {
57
66
  stepName: keyof Ramp;
58
67
  value: number;
59
68
  };
60
- debug?: boolean;
61
69
  } ) {
62
70
  const rampResults = {} as Record<
63
71
  keyof Ramp,
64
72
  { color: string; warning: boolean }
65
73
  >;
66
- let SATISFIED_ALL_CONTRAST_REQUIREMENTS = true;
67
- let UNSATISFIED_DIRECTION: RampDirection = 'lighter';
68
- let MAX_WEIGHTED_DEFICIT = 0;
74
+ let maxDeficit = -Infinity;
75
+ let maxDeficitDirection: RampDirection = 'lighter';
76
+ let maxDeficitStep;
69
77
 
70
78
  // Keep track of the calculated colors, as they are going to be useful
71
79
  // when other colors reference them.
72
- const calculatedColors = new Map< keyof Ramp | 'seed', Color >();
80
+ const calculatedColors = new Map< keyof Ramp | 'seed', ColorTypes >();
73
81
  calculatedColors.set( 'seed', seed );
74
82
 
75
83
  for ( const stepName of sortedSteps ) {
@@ -79,8 +87,8 @@ function calculateRamp( {
79
87
  taperChromaOptions,
80
88
  sameAsIfPossible,
81
89
  } = config[ stepName ];
82
- const referenceColor = calculatedColors.get( contrast.reference );
83
90
 
91
+ const referenceColor = calculatedColors.get( contrast.reference );
84
92
  if ( ! referenceColor ) {
85
93
  throw new Error(
86
94
  `Reference color for step ${ stepName } not found: ${ contrast.reference }`
@@ -90,28 +98,32 @@ function calculateRamp( {
90
98
  // Check if we can reuse color from the `sameAsIfPossible` config option
91
99
  if ( sameAsIfPossible ) {
92
100
  const candidateColor = calculatedColors.get( sameAsIfPossible );
93
- if ( candidateColor ) {
94
- const candidateContrast = getCachedContrast(
95
- referenceColor,
96
- candidateColor
101
+ if ( ! candidateColor ) {
102
+ throw new Error(
103
+ `Same-as color for step ${ stepName } not found: ${ sameAsIfPossible }`
97
104
  );
98
- const adjustedTarget = adjustContrastTarget( contrast.target );
99
- // If the candidate meets the contrast requirement, use it
100
- if ( candidateContrast >= adjustedTarget ) {
101
- // Store the reused color
102
- calculatedColors.set( stepName, candidateColor );
103
- rampResults[ stepName ] = {
104
- color: getColorString( candidateColor ),
105
- warning: false,
106
- };
107
-
108
- continue; // Skip to next step
109
- }
105
+ }
106
+
107
+ const candidateContrast = getContrast(
108
+ referenceColor,
109
+ candidateColor
110
+ );
111
+ const adjustedTarget = adjustContrastTarget( contrast.target );
112
+ // If the candidate meets the contrast requirement, use it
113
+ if ( candidateContrast >= adjustedTarget ) {
114
+ // Store the reused color
115
+ calculatedColors.set( stepName, candidateColor );
116
+ rampResults[ stepName ] = {
117
+ color: getColorString( candidateColor ),
118
+ warning: false,
119
+ };
120
+
121
+ continue; // Skip to next step
110
122
  }
111
123
  }
112
124
 
113
125
  function computeDirection(
114
- color: Color,
126
+ color: ColorTypes,
115
127
  followDirection: FollowDirection
116
128
  ): RampDirection {
117
129
  if ( followDirection === 'main' ) {
@@ -160,33 +172,21 @@ function calculateRamp( {
160
172
  adjustedTarget,
161
173
  computedDir,
162
174
  {
163
- strict: false,
164
175
  lightnessConstraint,
165
176
  taperChromaOptions,
166
- debug,
167
177
  }
168
178
  );
169
179
 
170
180
  // When the target contrast is not met, take note of it and use
171
181
  // that information to guide the ramp calculation bisection.
172
- if ( ! searchResults.reached && ! contrast.ignoreWhenAdjustingSeed ) {
173
- SATISFIED_ALL_CONTRAST_REQUIREMENTS = false;
174
-
175
- // Calculate constraint failure severity for seed optimization
176
- // Use the relative deficit size, weighted by how changing the seed would impact this constraint
177
- const deficitVsTarget = adjustedTarget - searchResults.achieved;
178
-
179
- // Weight the deficit by how much seed adjustment would help this constraint
180
- // If seed has low contrast vs reference, adjusting seed has high impact
181
- // If seed has high contrast vs reference, adjusting seed has low impact
182
- const impactWeight = 1 / getCachedContrast( seed, referenceColor );
183
- const weightedDeficit = deficitVsTarget * impactWeight;
184
-
185
- // Track the most impactful failure for seed optimization
186
- if ( weightedDeficit > MAX_WEIGHTED_DEFICIT ) {
187
- MAX_WEIGHTED_DEFICIT = weightedDeficit;
188
- UNSATISFIED_DIRECTION = computedDir;
189
- }
182
+ if (
183
+ ! contrast.ignoreWhenAdjustingSeed &&
184
+ searchResults.deficit &&
185
+ searchResults.deficit > maxDeficit
186
+ ) {
187
+ maxDeficit = searchResults.deficit;
188
+ maxDeficitDirection = computedDir;
189
+ maxDeficitStep = stepName;
190
190
  }
191
191
 
192
192
  // Store calculated color for future dependencies
@@ -199,11 +199,11 @@ function calculateRamp( {
199
199
  ! contrast.ignoreWhenAdjustingSeed && ! searchResults.reached,
200
200
  };
201
201
  }
202
-
203
202
  return {
204
203
  rampResults,
205
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
206
- UNSATISFIED_DIRECTION,
204
+ maxDeficit,
205
+ maxDeficitDirection,
206
+ maxDeficitStep,
207
207
  };
208
208
  }
209
209
 
@@ -213,7 +213,6 @@ export function buildRamp(
213
213
  {
214
214
  mainDirection,
215
215
  pinLightness,
216
- debug = false,
217
216
  rescaleToFitContrastTargets = true,
218
217
  }: {
219
218
  mainDirection?: RampDirection;
@@ -222,12 +221,11 @@ export function buildRamp(
222
221
  value: number;
223
222
  };
224
223
  rescaleToFitContrastTargets?: boolean;
225
- debug?: boolean;
226
224
  } = {}
227
225
  ): RampResult {
228
- let seed: Color;
226
+ let seed: PlainColorObject;
229
227
  try {
230
- seed = clampToGamut( new Color( seedArg ) );
228
+ seed = clampToGamut( parse( seedArg ) );
231
229
  } catch ( error ) {
232
230
  throw new Error(
233
231
  `Invalid seed color "${ seedArg }": ${
@@ -252,118 +250,82 @@ export function buildRamp(
252
250
  const sortedSteps = sortByDependency( config );
253
251
 
254
252
  // Calculate the ramp with the initial seed.
255
- const {
256
- rampResults,
257
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
258
- UNSATISFIED_DIRECTION,
259
- } = calculateRamp( {
260
- seed,
261
- sortedSteps,
262
- config,
263
- mainDir,
264
- oppDir,
265
- pinLightness,
266
- debug,
267
- } );
268
- const toReturn = {
269
- ramp: rampResults,
270
- direction: mainDir,
271
- } as RampResult;
272
-
273
- if ( debug ) {
274
- // eslint-disable-next-line no-console
275
- console.log( `First run`, {
276
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
277
- UNSATISFIED_DIRECTION,
278
- seed: seed.toString(),
253
+ const { rampResults, maxDeficit, maxDeficitDirection, maxDeficitStep } =
254
+ calculateRamp( {
255
+ seed,
279
256
  sortedSteps,
280
257
  config,
281
258
  mainDir,
282
259
  oppDir,
283
260
  pinLightness,
284
261
  } );
285
- }
286
262
 
287
- if (
288
- ! SATISFIED_ALL_CONTRAST_REQUIREMENTS &&
289
- rescaleToFitContrastTargets
290
- ) {
291
- let worseSeedL = seed.oklch.l;
292
- // For a scale with the "lighter" direction, the contrast can be improved
293
- // by darkening the seed. For "darker" direction, by lightening the seed.
294
- let betterSeedL = UNSATISFIED_DIRECTION === 'lighter' ? 0 : 1;
295
-
296
- // Binary search: try a new seed and recompute the whole ramp
297
- // (TODO: try a smarter approach?)
298
- for (
299
- let i = 0;
300
- i < MAX_BISECTION_ITERATIONS &&
301
- Math.abs( betterSeedL - worseSeedL ) > LIGHTNESS_EPSILON;
302
- i++
303
- ) {
304
- const newSeed = clampToGamut(
305
- seed.clone().set( {
306
- l: ( worseSeedL + betterSeedL ) / 2,
307
- } )
308
- );
263
+ let bestRamp = rampResults;
309
264
 
310
- if ( debug ) {
311
- // eslint-disable-next-line no-console
312
- console.log( `Iteration ${ i }`, {
313
- worseSeedL,
314
- newSeedL: ( worseSeedL + betterSeedL ) / 2,
315
- betterSeedL,
316
- } );
317
- }
265
+ if ( maxDeficit > CONTRAST_EPSILON && rescaleToFitContrastTargets ) {
266
+ const iterSteps = stepsForStep( maxDeficitStep!, config );
318
267
 
268
+ function getSeedForL( l: number ): ColorTypes {
269
+ return clampToGamut( set( clone( seed ), [ OKLCH, 'l' ], l ) );
270
+ }
271
+
272
+ function getDeficitForSeed( s: ColorTypes ): number {
319
273
  const iterationResults = calculateRamp( {
320
- seed: newSeed,
321
- sortedSteps,
274
+ seed: s,
275
+ sortedSteps: iterSteps,
322
276
  config,
323
277
  mainDir,
324
278
  oppDir,
325
279
  pinLightness,
326
- debug,
327
280
  } );
328
281
 
329
- if ( iterationResults.SATISFIED_ALL_CONTRAST_REQUIREMENTS ) {
330
- betterSeedL = newSeed.oklch.l;
331
- // Only update toReturn when the ramp satisfies all constraints.
332
- toReturn.ramp = iterationResults.rampResults;
333
- } else if ( UNSATISFIED_DIRECTION !== mainDir ) {
334
- // Failing constraint is in opposite direction to main ramp direction
335
- // We've moved too far in mainDir, constrain the search
336
- betterSeedL = newSeed.oklch.l;
337
- } else {
338
- // Failing constraint is in same direction as main ramp direction
339
- // We haven't moved far enough in mainDir, continue searching
340
- worseSeedL = newSeed.oklch.l;
341
- }
342
-
343
- if ( debug ) {
344
- // eslint-disable-next-line no-console
345
- console.log( `Retry #${ i }`, {
346
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
347
- UNSATISFIED_DIRECTION,
348
- seed: newSeed.toString(),
349
- sortedSteps,
350
- config,
351
- mainDir,
352
- oppDir,
353
- pinLightness,
354
- } );
355
- }
282
+ // If the constraints start failing in the opposite direction to the original
283
+ // iteration's direction, that means we've moved too far away from the target.
284
+ // Don't use the `maxDeficit` value because it's not related to our search,
285
+ // and might even be positive, which would confuse the bisection algorithm.
286
+ return iterationResults.maxDeficitDirection === maxDeficitDirection
287
+ ? iterationResults.maxDeficit
288
+ : -maxDeficit;
356
289
  }
290
+
291
+ // For a scale with the "lighter" direction, the contrast can be improved
292
+ // by darkening the seed. For "darker" direction, by lightening the seed.
293
+ const lowerSeedL = maxDeficitDirection === 'lighter' ? 0 : 1;
294
+ const lowerDeficit = -maxDeficit;
295
+ const upperSeedL = get( seed, [ OKLCH, 'l' ] );
296
+ const upperDeficit = maxDeficit;
297
+
298
+ const bestSeed = solveWithBisect(
299
+ getSeedForL,
300
+ getDeficitForSeed,
301
+ lowerSeedL,
302
+ lowerDeficit,
303
+ upperSeedL,
304
+ upperDeficit
305
+ );
306
+
307
+ // Calculate the final ramp with adjusted seed.
308
+ bestRamp = calculateRamp( {
309
+ seed: bestSeed,
310
+ sortedSteps,
311
+ config,
312
+ mainDir,
313
+ oppDir,
314
+ pinLightness,
315
+ } ).rampResults;
357
316
  }
358
317
 
359
318
  // Swap surface1 and surface3 for darker ramps to maintain visual elevation hierarchy.
360
319
  // This ensures surface1 appears "behind" surface2, and surface3 appears "in front",
361
320
  // regardless of the ramp's main direction.
362
321
  if ( mainDir === 'darker' ) {
363
- const tmpSurface1 = toReturn.ramp.surface1;
364
- toReturn.ramp.surface1 = toReturn.ramp.surface3;
365
- toReturn.ramp.surface3 = tmpSurface1;
322
+ const tmpSurface1 = bestRamp.surface1;
323
+ bestRamp.surface1 = bestRamp.surface3;
324
+ bestRamp.surface3 = tmpSurface1;
366
325
  }
367
326
 
368
- return toReturn;
327
+ return {
328
+ ramp: bestRamp,
329
+ direction: mainDir,
330
+ };
369
331
  }
@@ -59,7 +59,7 @@ export const BG_RAMP_CONFIG: RampConfig = {
59
59
  contrast: {
60
60
  reference: 'surface2',
61
61
  followDirection: 'opposite',
62
- target: 1.02,
62
+ target: 1.06,
63
63
  ignoreWhenAdjustingSeed: true,
64
64
  },
65
65
  taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
@@ -75,7 +75,7 @@ export const BG_RAMP_CONFIG: RampConfig = {
75
75
  contrast: {
76
76
  reference: 'surface2',
77
77
  followDirection: 'main',
78
- target: 1.02,
78
+ target: 1.06,
79
79
  },
80
80
  taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
81
81
  },
@@ -83,7 +83,7 @@ export const BG_RAMP_CONFIG: RampConfig = {
83
83
  contrast: {
84
84
  reference: 'surface2',
85
85
  followDirection: 'main',
86
- target: 1.08,
86
+ target: 1.12,
87
87
  },
88
88
  taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
89
89
  },