@wordpress/theme 0.1.1-next.2f1c7c01b.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 (91) hide show
  1. package/bin/terrazzo-plugin-ds-tokens-docs/index.ts +5 -24
  2. package/bin/terrazzo-plugin-inline-alias-values/index.ts +84 -0
  3. package/bin/terrazzo-plugin-known-wpds-css-variables/index.ts +19 -39
  4. package/build/color-ramps/lib/constants.js +4 -4
  5. package/build/color-ramps/lib/constants.js.map +2 -2
  6. package/build/color-ramps/lib/default-ramps.js +82 -82
  7. package/build/color-ramps/lib/default-ramps.js.map +1 -1
  8. package/build/color-ramps/lib/find-color-with-constraints.js +36 -53
  9. package/build/color-ramps/lib/find-color-with-constraints.js.map +2 -2
  10. package/build/color-ramps/lib/index.js +64 -63
  11. package/build/color-ramps/lib/index.js.map +2 -2
  12. package/build/color-ramps/lib/ramp-configs.js +3 -3
  13. package/build/color-ramps/lib/ramp-configs.js.map +1 -1
  14. package/build/color-ramps/lib/utils.js +63 -2
  15. package/build/color-ramps/lib/utils.js.map +2 -2
  16. package/build/prebuilt/js/design-tokens.js +5 -10
  17. package/build/prebuilt/js/design-tokens.js.map +2 -2
  18. package/build/prebuilt/json/figma.json +105 -905
  19. package/build/prebuilt/ts/color-tokens.js +137 -0
  20. package/build/prebuilt/ts/color-tokens.js.map +7 -0
  21. package/build/token-id.js +30 -0
  22. package/build/token-id.js.map +7 -0
  23. package/build/use-theme-provider-styles.js +18 -27
  24. package/build/use-theme-provider-styles.js.map +3 -3
  25. package/build-module/color-ramps/lib/constants.js +3 -3
  26. package/build-module/color-ramps/lib/constants.js.map +2 -2
  27. package/build-module/color-ramps/lib/default-ramps.js +82 -82
  28. package/build-module/color-ramps/lib/default-ramps.js.map +1 -1
  29. package/build-module/color-ramps/lib/find-color-with-constraints.js +38 -60
  30. package/build-module/color-ramps/lib/find-color-with-constraints.js.map +2 -2
  31. package/build-module/color-ramps/lib/index.js +68 -65
  32. package/build-module/color-ramps/lib/index.js.map +2 -2
  33. package/build-module/color-ramps/lib/ramp-configs.js +3 -3
  34. package/build-module/color-ramps/lib/ramp-configs.js.map +1 -1
  35. package/build-module/color-ramps/lib/utils.js +63 -2
  36. package/build-module/color-ramps/lib/utils.js.map +2 -2
  37. package/build-module/prebuilt/js/design-tokens.js +5 -10
  38. package/build-module/prebuilt/js/design-tokens.js.map +2 -2
  39. package/build-module/prebuilt/json/figma.json +105 -905
  40. package/build-module/prebuilt/ts/color-tokens.js +117 -0
  41. package/build-module/prebuilt/ts/color-tokens.js.map +7 -0
  42. package/build-module/token-id.js +6 -0
  43. package/build-module/token-id.js.map +7 -0
  44. package/build-module/use-theme-provider-styles.js +18 -27
  45. package/build-module/use-theme-provider-styles.js.map +2 -2
  46. package/build-types/color-ramps/lib/constants.d.ts +2 -2
  47. package/build-types/color-ramps/lib/constants.d.ts.map +1 -1
  48. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts +2 -3
  49. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts.map +1 -1
  50. package/build-types/color-ramps/lib/index.d.ts.map +1 -1
  51. package/build-types/color-ramps/lib/utils.d.ts +21 -2
  52. package/build-types/color-ramps/lib/utils.d.ts.map +1 -1
  53. package/build-types/color-ramps/stories/index.story.d.ts.map +1 -1
  54. package/build-types/prebuilt/ts/color-tokens.d.ts +7 -0
  55. package/build-types/prebuilt/ts/color-tokens.d.ts.map +1 -0
  56. package/build-types/stories/index.story.d.ts.map +1 -1
  57. package/build-types/theme-provider.d.ts.map +1 -1
  58. package/build-types/token-id.d.ts +9 -0
  59. package/build-types/token-id.d.ts.map +1 -0
  60. package/build-types/use-theme-provider-styles.d.ts.map +1 -1
  61. package/docs/ds-tokens.md +10 -178
  62. package/package.json +4 -4
  63. package/src/color-ramps/lib/constants.ts +7 -5
  64. package/src/color-ramps/lib/default-ramps.ts +82 -82
  65. package/src/color-ramps/lib/find-color-with-constraints.ts +53 -77
  66. package/src/color-ramps/lib/index.ts +98 -102
  67. package/src/color-ramps/lib/ramp-configs.ts +3 -3
  68. package/src/color-ramps/lib/utils.ts +109 -5
  69. package/src/color-ramps/test/__snapshots__/index.test.ts.snap +45706 -360
  70. package/src/color-ramps/test/index.test.ts +41 -14
  71. package/src/prebuilt/css/design-tokens.css +88 -413
  72. package/src/prebuilt/js/design-tokens.js +5 -10
  73. package/src/prebuilt/json/figma.json +105 -905
  74. package/src/prebuilt/ts/color-tokens.ts +117 -0
  75. package/src/stories/index.story.tsx +4 -18
  76. package/src/test/token-id.test.ts +12 -0
  77. package/src/token-id.ts +9 -0
  78. package/src/use-theme-provider-styles.ts +20 -35
  79. package/terrazzo.config.ts +15 -12
  80. package/tokens/color.json +82 -82
  81. package/tokens/dimension.json +75 -0
  82. package/tsconfig.bin.tsbuildinfo +1 -1
  83. package/tsconfig.src.tsbuildinfo +1 -1
  84. package/build/prebuilt/ts/design-tokens.js +0 -391
  85. package/build/prebuilt/ts/design-tokens.js.map +0 -7
  86. package/build-module/prebuilt/ts/design-tokens.js +0 -371
  87. package/build-module/prebuilt/ts/design-tokens.js.map +0 -7
  88. package/build-types/prebuilt/ts/design-tokens.d.ts +0 -7
  89. package/build-types/prebuilt/ts/design-tokens.d.ts.map +0 -1
  90. package/src/prebuilt/ts/design-tokens.ts +0 -371
  91. package/tokens/spacing.json +0 -45
@@ -7,16 +7,22 @@ import { get, OKLCH, type ColorTypes } from 'colorjs.io/fn';
7
7
  * Internal dependencies
8
8
  */
9
9
  import './register-color-spaces';
10
- import { clampToGamut } from './utils';
11
- import {
12
- WHITE,
13
- BLACK,
14
- LIGHTNESS_EPSILON,
15
- MAX_BISECTION_ITERATIONS,
16
- } from './constants';
10
+ import { clampToGamut, solveWithBisect } from './utils';
11
+ import { WHITE, BLACK, CONTRAST_EPSILON } from './constants';
17
12
  import { getContrast } from './color-utils';
18
13
  import { type TaperChromaOptions, taperChroma } from './taper-chroma';
19
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
+
20
26
  /**
21
27
  * Solve for L such that:
22
28
  * - the L applied to the seed meets the contrast target against the reference
@@ -28,7 +34,6 @@ import { type TaperChromaOptions, taperChroma } from './taper-chroma';
28
34
  * @param target
29
35
  * @param direction
30
36
  * @param options
31
- * @param options.strict
32
37
  * @param options.lightnessConstraint
33
38
  * @param options.lightnessConstraint.type
34
39
  * @param options.lightnessConstraint.value
@@ -42,20 +47,22 @@ export function findColorMeetingRequirements(
42
47
  {
43
48
  lightnessConstraint,
44
49
  taperChromaOptions,
45
- strict = true,
46
50
  }: {
47
51
  lightnessConstraint?: {
48
52
  type: 'force' | 'onlyIfSucceeds';
49
53
  value: number;
50
54
  };
51
55
  taperChromaOptions?: TaperChromaOptions;
52
- strict?: boolean;
53
56
  } = {}
54
- ): { color: ColorTypes; reached: boolean; achieved: number } {
57
+ ): { color: ColorTypes; reached: boolean; achieved: number; deficit?: number } {
55
58
  // A target of 1 means same color.
56
59
  // A target lower than 1 doesn't make sense.
57
60
  if ( target <= 1 ) {
58
- return { color: seed, reached: true, achieved: 1 };
61
+ return {
62
+ color: reference,
63
+ reached: true,
64
+ achieved: 1,
65
+ };
59
66
  }
60
67
 
61
68
  function getColorForL( l: number ): ColorTypes {
@@ -80,48 +87,45 @@ export function findColorMeetingRequirements(
80
87
  } );
81
88
  }
82
89
 
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
+
83
95
  if ( lightnessConstraint ) {
84
96
  // Apply a specific L value.
85
97
  // Useful when pinning a step to a specific lightness, of to specify
86
98
  // min/max L values.
87
99
  const colorWithExactL = getColorForL( lightnessConstraint.value );
88
100
  const exactLContrast = getContrast( reference, colorWithExactL );
101
+ const exactLContrastMeetsTarget =
102
+ cdiff( exactLContrast, target ) >= -CONTRAST_EPSILON;
89
103
 
90
104
  // If the L constraint is of "force" type, apply it even when it doesn't
91
105
  // meet the contrast target.
92
106
  if (
93
- lightnessConstraint.type === 'force' ||
94
- exactLContrast >= target
107
+ exactLContrastMeetsTarget ||
108
+ lightnessConstraint.type === 'force'
95
109
  ) {
96
110
  return {
97
111
  color: colorWithExactL,
98
- reached: exactLContrast >= target,
112
+ reached: exactLContrastMeetsTarget,
99
113
  achieved: exactLContrast,
114
+ deficit: exactLContrastMeetsTarget
115
+ ? cdiff( exactLContrast, highestContrast )
116
+ : cdiff( target, exactLContrast ),
100
117
  };
101
118
  }
102
119
  }
103
120
 
104
- // Set the boundary based on the direction.
105
- const mostContrastingL = direction === 'lighter' ? 1 : 0;
106
- const mostContrastingColor = direction === 'lighter' ? WHITE : BLACK;
107
- const highestContrast = getContrast( reference, mostContrastingColor );
108
-
109
- // If even the most contrasting color can't reach the target,
110
- // the target is unreachable.
111
- if ( highestContrast < target ) {
112
- if ( strict ) {
113
- throw new Error(
114
- `Contrast target ${ target.toFixed(
115
- 2
116
- ) }:1 unreachable in ${ direction } direction` +
117
- `(boundary achieves ${ highestContrast.toFixed( 3 ) }:1).`
118
- );
119
- }
120
-
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 ) {
121
124
  return {
122
125
  color: mostContrastingColor,
123
- reached: false,
126
+ reached: cdiff( highestContrast, target ) >= -CONTRAST_EPSILON,
124
127
  achieved: highestContrast,
128
+ deficit: cdiff( target, highestContrast ),
125
129
  };
126
130
  }
127
131
 
@@ -129,53 +133,25 @@ export function findColorMeetingRequirements(
129
133
  // Originally this was seed.oklch.l — although it's an assumption that works
130
134
  // only when we know for sure the direction of the search.
131
135
  // TODO: can we bring this back to seed.oklch.l ?
132
- let worseL = get( reference, [ OKLCH, 'l' ] );
133
- let worseContrast = 1;
134
- let replacedWorse = false;
135
- let betterL = mostContrastingL;
136
- let betterContrast = highestContrast;
137
- let replacedBetter = false;
138
-
139
- let bestColor: ColorTypes = mostContrastingColor;
140
- let bestContrast = highestContrast;
141
-
142
- for ( let i = 0; i < MAX_BISECTION_ITERATIONS; i++ ) {
143
- // Linear interpolation between worse and better L values, weighted by the contrast difference.
144
- const newL =
145
- ( worseL * ( betterContrast - target ) -
146
- betterL * ( worseContrast - target ) ) /
147
- ( betterContrast - worseContrast );
148
-
149
- bestColor = getColorForL( newL );
150
- bestContrast = getContrast( reference, bestColor );
151
-
152
- if ( Math.abs( bestContrast - target ) <= LIGHTNESS_EPSILON ) {
153
- break;
154
- }
155
-
156
- // Update one of the boundary L values, using the Illinois method.
157
- if ( bestContrast >= target ) {
158
- betterL = newL;
159
- betterContrast = bestContrast;
160
- if ( replacedBetter ) {
161
- worseContrast = ( worseContrast + target ) / 2;
162
- }
163
- replacedBetter = true;
164
- replacedWorse = false;
165
- } else {
166
- worseL = newL;
167
- worseContrast = bestContrast;
168
- if ( replacedWorse ) {
169
- betterContrast = ( betterContrast + target ) / 2;
170
- }
171
- replacedWorse = true;
172
- replacedBetter = false;
173
- }
174
- }
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
+ );
175
149
 
176
150
  return {
177
151
  color: bestColor,
178
152
  reached: true,
179
- achieved: bestContrast,
153
+ achieved: target,
154
+ // Negative number that specifies how much room we have.
155
+ deficit: cdiff( target, highestContrast ),
180
156
  };
181
157
  }
@@ -22,6 +22,8 @@ import {
22
22
  sortByDependency,
23
23
  computeBetterFgColorDirection,
24
24
  adjustContrastTarget,
25
+ stepsForStep,
26
+ solveWithBisect,
25
27
  } from './utils';
26
28
 
27
29
  import type {
@@ -31,7 +33,7 @@ import type {
31
33
  RampConfig,
32
34
  RampResult,
33
35
  } from './types';
34
- import { LIGHTNESS_EPSILON, MAX_BISECTION_ITERATIONS } from './constants';
36
+ import { CONTRAST_EPSILON } from './constants';
35
37
 
36
38
  /**
37
39
  * Calculate a complete color ramp based on the provided configuration.
@@ -69,9 +71,9 @@ function calculateRamp( {
69
71
  keyof Ramp,
70
72
  { color: string; warning: boolean }
71
73
  >;
72
- let SATISFIED_ALL_CONTRAST_REQUIREMENTS = true;
73
- let UNSATISFIED_DIRECTION: RampDirection = 'lighter';
74
- let MAX_WEIGHTED_DEFICIT = 0;
74
+ let maxDeficit = -Infinity;
75
+ let maxDeficitDirection: RampDirection = 'lighter';
76
+ let maxDeficitStep;
75
77
 
76
78
  // Keep track of the calculated colors, as they are going to be useful
77
79
  // when other colors reference them.
@@ -85,8 +87,8 @@ function calculateRamp( {
85
87
  taperChromaOptions,
86
88
  sameAsIfPossible,
87
89
  } = config[ stepName ];
88
- const referenceColor = calculatedColors.get( contrast.reference );
89
90
 
91
+ const referenceColor = calculatedColors.get( contrast.reference );
90
92
  if ( ! referenceColor ) {
91
93
  throw new Error(
92
94
  `Reference color for step ${ stepName } not found: ${ contrast.reference }`
@@ -96,23 +98,27 @@ function calculateRamp( {
96
98
  // Check if we can reuse color from the `sameAsIfPossible` config option
97
99
  if ( sameAsIfPossible ) {
98
100
  const candidateColor = calculatedColors.get( sameAsIfPossible );
99
- if ( candidateColor ) {
100
- const candidateContrast = getContrast(
101
- referenceColor,
102
- candidateColor
101
+ if ( ! candidateColor ) {
102
+ throw new Error(
103
+ `Same-as color for step ${ stepName } not found: ${ sameAsIfPossible }`
103
104
  );
104
- const adjustedTarget = adjustContrastTarget( contrast.target );
105
- // If the candidate meets the contrast requirement, use it
106
- if ( candidateContrast >= adjustedTarget ) {
107
- // Store the reused color
108
- calculatedColors.set( stepName, candidateColor );
109
- rampResults[ stepName ] = {
110
- color: getColorString( candidateColor ),
111
- warning: false,
112
- };
113
-
114
- continue; // Skip to next step
115
- }
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
116
122
  }
117
123
  }
118
124
 
@@ -166,7 +172,6 @@ function calculateRamp( {
166
172
  adjustedTarget,
167
173
  computedDir,
168
174
  {
169
- strict: false,
170
175
  lightnessConstraint,
171
176
  taperChromaOptions,
172
177
  }
@@ -174,24 +179,14 @@ function calculateRamp( {
174
179
 
175
180
  // When the target contrast is not met, take note of it and use
176
181
  // that information to guide the ramp calculation bisection.
177
- if ( ! searchResults.reached && ! contrast.ignoreWhenAdjustingSeed ) {
178
- SATISFIED_ALL_CONTRAST_REQUIREMENTS = false;
179
-
180
- // Calculate constraint failure severity for seed optimization
181
- // Use the relative deficit size, weighted by how changing the seed would impact this constraint
182
- const deficitVsTarget = adjustedTarget - searchResults.achieved;
183
-
184
- // Weight the deficit by how much seed adjustment would help this constraint
185
- // If seed has low contrast vs reference, adjusting seed has high impact
186
- // If seed has high contrast vs reference, adjusting seed has low impact
187
- const impactWeight = 1 / getContrast( seed, referenceColor );
188
- const weightedDeficit = deficitVsTarget * impactWeight;
189
-
190
- // Track the most impactful failure for seed optimization
191
- if ( weightedDeficit > MAX_WEIGHTED_DEFICIT ) {
192
- MAX_WEIGHTED_DEFICIT = weightedDeficit;
193
- UNSATISFIED_DIRECTION = computedDir;
194
- }
182
+ if (
183
+ ! contrast.ignoreWhenAdjustingSeed &&
184
+ searchResults.deficit &&
185
+ searchResults.deficit > maxDeficit
186
+ ) {
187
+ maxDeficit = searchResults.deficit;
188
+ maxDeficitDirection = computedDir;
189
+ maxDeficitStep = stepName;
195
190
  }
196
191
 
197
192
  // Store calculated color for future dependencies
@@ -204,11 +199,11 @@ function calculateRamp( {
204
199
  ! contrast.ignoreWhenAdjustingSeed && ! searchResults.reached,
205
200
  };
206
201
  }
207
-
208
202
  return {
209
203
  rampResults,
210
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
211
- UNSATISFIED_DIRECTION,
204
+ maxDeficit,
205
+ maxDeficitDirection,
206
+ maxDeficitStep,
212
207
  };
213
208
  }
214
209
 
@@ -255,81 +250,82 @@ export function buildRamp(
255
250
  const sortedSteps = sortByDependency( config );
256
251
 
257
252
  // Calculate the ramp with the initial seed.
258
- const {
259
- rampResults,
260
- SATISFIED_ALL_CONTRAST_REQUIREMENTS,
261
- UNSATISFIED_DIRECTION,
262
- } = calculateRamp( {
263
- seed,
264
- sortedSteps,
265
- config,
266
- mainDir,
267
- oppDir,
268
- pinLightness,
269
- } );
270
- const toReturn = {
271
- ramp: rampResults,
272
- direction: mainDir,
273
- } as RampResult;
253
+ const { rampResults, maxDeficit, maxDeficitDirection, maxDeficitStep } =
254
+ calculateRamp( {
255
+ seed,
256
+ sortedSteps,
257
+ config,
258
+ mainDir,
259
+ oppDir,
260
+ pinLightness,
261
+ } );
274
262
 
275
- if (
276
- ! SATISFIED_ALL_CONTRAST_REQUIREMENTS &&
277
- rescaleToFitContrastTargets
278
- ) {
279
- let worseSeedL = get( seed, [ OKLCH, 'l' ] );
280
- // For a scale with the "lighter" direction, the contrast can be improved
281
- // by darkening the seed. For "darker" direction, by lightening the seed.
282
- let betterSeedL = UNSATISFIED_DIRECTION === 'lighter' ? 0 : 1;
283
-
284
- // Binary search: try a new seed and recompute the whole ramp
285
- // (TODO: try a smarter approach?)
286
- for (
287
- let i = 0;
288
- i < MAX_BISECTION_ITERATIONS &&
289
- Math.abs( betterSeedL - worseSeedL ) > LIGHTNESS_EPSILON;
290
- i++
291
- ) {
292
- const newSeed = clampToGamut(
293
- set(
294
- clone( seed ),
295
- [ OKLCH, 'l' ],
296
- ( worseSeedL + betterSeedL ) / 2
297
- )
298
- );
263
+ let bestRamp = rampResults;
299
264
 
265
+ if ( maxDeficit > CONTRAST_EPSILON && rescaleToFitContrastTargets ) {
266
+ const iterSteps = stepsForStep( maxDeficitStep!, config );
267
+
268
+ function getSeedForL( l: number ): ColorTypes {
269
+ return clampToGamut( set( clone( seed ), [ OKLCH, 'l' ], l ) );
270
+ }
271
+
272
+ function getDeficitForSeed( s: ColorTypes ): number {
300
273
  const iterationResults = calculateRamp( {
301
- seed: newSeed,
302
- sortedSteps,
274
+ seed: s,
275
+ sortedSteps: iterSteps,
303
276
  config,
304
277
  mainDir,
305
278
  oppDir,
306
279
  pinLightness,
307
280
  } );
308
281
 
309
- if ( iterationResults.SATISFIED_ALL_CONTRAST_REQUIREMENTS ) {
310
- betterSeedL = get( newSeed, [ OKLCH, 'l' ] );
311
- // Only update toReturn when the ramp satisfies all constraints.
312
- toReturn.ramp = iterationResults.rampResults;
313
- } else if ( UNSATISFIED_DIRECTION !== mainDir ) {
314
- // Failing constraint is in opposite direction to main ramp direction
315
- // We've moved too far in mainDir, constrain the search
316
- betterSeedL = get( newSeed, [ OKLCH, 'l' ] );
317
- } else {
318
- // Failing constraint is in same direction as main ramp direction
319
- // We haven't moved far enough in mainDir, continue searching
320
- worseSeedL = get( newSeed, [ OKLCH, 'l' ] );
321
- }
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;
322
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;
323
316
  }
324
317
 
325
318
  // Swap surface1 and surface3 for darker ramps to maintain visual elevation hierarchy.
326
319
  // This ensures surface1 appears "behind" surface2, and surface3 appears "in front",
327
320
  // regardless of the ramp's main direction.
328
321
  if ( mainDir === 'darker' ) {
329
- const tmpSurface1 = toReturn.ramp.surface1;
330
- toReturn.ramp.surface1 = toReturn.ramp.surface3;
331
- toReturn.ramp.surface3 = tmpSurface1;
322
+ const tmpSurface1 = bestRamp.surface1;
323
+ bestRamp.surface1 = bestRamp.surface3;
324
+ bestRamp.surface3 = tmpSurface1;
332
325
  }
333
326
 
334
- return toReturn;
327
+ return {
328
+ ramp: bestRamp,
329
+ direction: mainDir,
330
+ };
335
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
  },
@@ -13,8 +13,10 @@ import {
13
13
  UNIVERSAL_CONTRAST_TOPUP,
14
14
  WHITE_TEXT_CONTRAST_MARGIN,
15
15
  ACCENT_SCALE_BASE_LIGHTNESS_THRESHOLDS,
16
+ MAX_BISECTION_ITERATIONS,
17
+ CONTRAST_EPSILON,
16
18
  } from './constants';
17
- import type { Ramp, RampStepConfig, RampDirection } from './types';
19
+ import type { Ramp, RampConfig, RampDirection } from './types';
18
20
  import { getContrast } from './color-utils';
19
21
 
20
22
  /**
@@ -28,7 +30,7 @@ export const clampToGamut = ( c: ColorTypes ) =>
28
30
  * Build a dependency graph from the steps configuration
29
31
  * @param config - The steps configuration object
30
32
  */
31
- function buildDependencyGraph( config: Record< keyof Ramp, RampStepConfig > ): {
33
+ function buildDependencyGraph( config: RampConfig ): {
32
34
  dependencies: Map< keyof Ramp, ( keyof Ramp | 'seed' )[] >;
33
35
  dependents: Map< keyof Ramp | 'seed', ( keyof Ramp )[] >;
34
36
  } {
@@ -66,9 +68,7 @@ function buildDependencyGraph( config: Record< keyof Ramp, RampStepConfig > ): {
66
68
  * Topologically sort steps based on their dependencies
67
69
  * @param config - The steps configuration object
68
70
  */
69
- export function sortByDependency(
70
- config: Record< keyof Ramp, RampStepConfig >
71
- ): ( keyof Ramp )[] {
71
+ export function sortByDependency( config: RampConfig ): ( keyof Ramp )[] {
72
72
  const { dependents } = buildDependencyGraph( config );
73
73
  const result: ( keyof Ramp )[] = [];
74
74
  const visited = new Set< keyof Ramp | 'seed' >();
@@ -108,6 +108,37 @@ export function sortByDependency(
108
108
 
109
109
  return result;
110
110
  }
111
+ /**
112
+ * Return minimal set of steps that are needed to calculate `stepName` from the seed.
113
+ * @param stepName Name of the step.
114
+ * @param config Configuration of the ramp.
115
+ * @return Array of steps that `stepName` depends on.
116
+ */
117
+ export function stepsForStep(
118
+ stepName: keyof Ramp,
119
+ config: RampConfig
120
+ ): ( keyof Ramp )[] {
121
+ const result = new Set< keyof Ramp >();
122
+ function visit( step: keyof Ramp | 'seed' ) {
123
+ if ( step === 'seed' || result.has( step ) ) {
124
+ return;
125
+ }
126
+
127
+ const stepConfig = config[ step ];
128
+ if ( ! stepConfig ) {
129
+ return;
130
+ }
131
+
132
+ visit( stepConfig.contrast.reference );
133
+ if ( stepConfig.sameAsIfPossible ) {
134
+ visit( stepConfig.sameAsIfPossible );
135
+ }
136
+
137
+ result.add( step );
138
+ }
139
+ visit( stepName );
140
+ return Array.from( result );
141
+ }
111
142
 
112
143
  /**
113
144
  * Finds out whether a lighter or a darker foreground color achieves a better
@@ -158,3 +189,76 @@ export function clampAccentScaleReferenceLightness(
158
189
  const thresholds = ACCENT_SCALE_BASE_LIGHTNESS_THRESHOLDS[ direction ];
159
190
  return Math.max( thresholds.min, Math.min( thresholds.max, rawLightness ) );
160
191
  }
192
+
193
+ /**
194
+ * Find the value of of `L` (luminance) that produces a `C` (color) that has a
195
+ * `value` (contrast delta) equal to zero.
196
+ * @param calculateC Calculate `C` from a given `L`.
197
+ * @param calculateValue Calculate value (delta) for a given `C`.
198
+ * @param initLowerL Initial lower value of `L`.
199
+ * @param initLowerValue Initial lower delta (negative).
200
+ * @param initUpperL Initial upper value of `L`.
201
+ * @param initUpperValue Initial upper delta (positive).
202
+ * @return Resulting value of type `C`.
203
+ */
204
+ export function solveWithBisect< C >(
205
+ calculateC: ( l: number ) => C,
206
+ calculateValue: ( t: C ) => number,
207
+ initLowerL: number,
208
+ initLowerValue: number,
209
+ initUpperL: number,
210
+ initUpperValue: number
211
+ ): C {
212
+ let lowerL = initLowerL;
213
+ let lowerValue = initLowerValue;
214
+ let lowerReplaced = false;
215
+
216
+ let upperL = initUpperL;
217
+ let upperValue = initUpperValue;
218
+ let upperReplaced = false;
219
+
220
+ let bestC: C;
221
+ let bestValue: number;
222
+ let iterations = 0;
223
+
224
+ while ( true ) {
225
+ iterations++;
226
+
227
+ // Linear interpolation: find the point where a line would cross the zero axis.
228
+ const newL =
229
+ ( lowerL * upperValue - upperL * lowerValue ) /
230
+ ( upperValue - lowerValue );
231
+
232
+ bestC = calculateC( newL );
233
+ bestValue = calculateValue( bestC );
234
+
235
+ if (
236
+ Math.abs( bestValue ) <= CONTRAST_EPSILON ||
237
+ iterations >= MAX_BISECTION_ITERATIONS
238
+ ) {
239
+ break;
240
+ }
241
+
242
+ // Update the lower/upper bracket values. When only one side is repeatedly updated,
243
+ // apply so-called "Illinois trick" for faster convergence: halve the opposite value.
244
+ if ( bestValue <= 0 ) {
245
+ lowerL = newL;
246
+ lowerValue = bestValue;
247
+ if ( lowerReplaced ) {
248
+ upperValue /= 2;
249
+ }
250
+ lowerReplaced = true;
251
+ upperReplaced = false;
252
+ } else {
253
+ upperL = newL;
254
+ upperValue = bestValue;
255
+ if ( upperReplaced ) {
256
+ lowerValue /= 2;
257
+ }
258
+ upperReplaced = true;
259
+ lowerReplaced = false;
260
+ }
261
+ }
262
+
263
+ return bestC;
264
+ }