@wordpress/theme 0.1.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/LICENSE.md +788 -0
  2. package/README.md +67 -0
  3. package/bin/build-tokens.js +83 -0
  4. package/bin/generate-primitive-tokens/index.ts +115 -0
  5. package/bin/terrazzo-plugin-ds-tokens-docs/index.ts +103 -0
  6. package/bin/terrazzo-plugin-figma-ds-token-manager/index.ts +210 -0
  7. package/bin/terrazzo-plugin-figma-ds-token-manager/lib.ts +1 -0
  8. package/bin/terrazzo-plugin-known-wpds-css-variables/index.ts +72 -0
  9. package/build/color-ramps/index.js +132 -0
  10. package/build/color-ramps/index.js.map +7 -0
  11. package/build/color-ramps/lib/cache-utils.js +57 -0
  12. package/build/color-ramps/lib/cache-utils.js.map +7 -0
  13. package/build/color-ramps/lib/constants.js +105 -0
  14. package/build/color-ramps/lib/constants.js.map +7 -0
  15. package/build/color-ramps/lib/find-color-with-constraints.js +141 -0
  16. package/build/color-ramps/lib/find-color-with-constraints.js.map +7 -0
  17. package/build/color-ramps/lib/index.js +264 -0
  18. package/build/color-ramps/lib/index.js.map +7 -0
  19. package/build/color-ramps/lib/ramp-configs.js +315 -0
  20. package/build/color-ramps/lib/ramp-configs.js.map +7 -0
  21. package/build/color-ramps/lib/taper-chroma.js +159 -0
  22. package/build/color-ramps/lib/taper-chroma.js.map +7 -0
  23. package/build/color-ramps/lib/types.js +17 -0
  24. package/build/color-ramps/lib/types.js.map +7 -0
  25. package/build/color-ramps/lib/utils.js +106 -0
  26. package/build/color-ramps/lib/utils.js.map +7 -0
  27. package/build/context.js +34 -0
  28. package/build/context.js.map +7 -0
  29. package/build/index.js +29 -0
  30. package/build/index.js.map +7 -0
  31. package/build/lock-unlock.js +35 -0
  32. package/build/lock-unlock.js.map +7 -0
  33. package/build/prebuilt/js/design-tokens.js +135 -0
  34. package/build/prebuilt/js/design-tokens.js.map +7 -0
  35. package/build/prebuilt/json/figma.json +1317 -0
  36. package/build/prebuilt/ts/design-tokens.js +354 -0
  37. package/build/prebuilt/ts/design-tokens.js.map +7 -0
  38. package/build/private-apis.js +36 -0
  39. package/build/private-apis.js.map +7 -0
  40. package/build/style.module.css.js +2 -0
  41. package/build/theme-provider.js +92 -0
  42. package/build/theme-provider.js.map +7 -0
  43. package/build/types/css-modules.d.js +2 -0
  44. package/build/types/css-modules.d.js.map +7 -0
  45. package/build/types.js +17 -0
  46. package/build/types.js.map +7 -0
  47. package/build/use-theme-provider-styles.js +230 -0
  48. package/build/use-theme-provider-styles.js.map +7 -0
  49. package/build-module/color-ramps/index.js +95 -0
  50. package/build-module/color-ramps/index.js.map +7 -0
  51. package/build-module/color-ramps/lib/cache-utils.js +31 -0
  52. package/build-module/color-ramps/lib/cache-utils.js.map +7 -0
  53. package/build-module/color-ramps/lib/constants.js +63 -0
  54. package/build-module/color-ramps/lib/constants.js.map +7 -0
  55. package/build-module/color-ramps/lib/find-color-with-constraints.js +112 -0
  56. package/build-module/color-ramps/lib/find-color-with-constraints.js.map +7 -0
  57. package/build-module/color-ramps/lib/index.js +235 -0
  58. package/build-module/color-ramps/lib/index.js.map +7 -0
  59. package/build-module/color-ramps/lib/ramp-configs.js +290 -0
  60. package/build-module/color-ramps/lib/ramp-configs.js.map +7 -0
  61. package/build-module/color-ramps/lib/taper-chroma.js +125 -0
  62. package/build-module/color-ramps/lib/taper-chroma.js.map +7 -0
  63. package/build-module/color-ramps/lib/types.js +1 -0
  64. package/build-module/color-ramps/lib/types.js.map +7 -0
  65. package/build-module/color-ramps/lib/utils.js +84 -0
  66. package/build-module/color-ramps/lib/utils.js.map +7 -0
  67. package/build-module/context.js +10 -0
  68. package/build-module/context.js.map +7 -0
  69. package/build-module/index.js +5 -0
  70. package/build-module/index.js.map +7 -0
  71. package/build-module/lock-unlock.js +10 -0
  72. package/build-module/lock-unlock.js.map +7 -0
  73. package/build-module/prebuilt/js/design-tokens.js +115 -0
  74. package/build-module/prebuilt/js/design-tokens.js.map +7 -0
  75. package/build-module/prebuilt/json/figma.json +1317 -0
  76. package/build-module/prebuilt/ts/design-tokens.js +334 -0
  77. package/build-module/prebuilt/ts/design-tokens.js.map +7 -0
  78. package/build-module/private-apis.js +12 -0
  79. package/build-module/private-apis.js.map +7 -0
  80. package/build-module/style.module.css.js +1 -0
  81. package/build-module/theme-provider.js +58 -0
  82. package/build-module/theme-provider.js.map +7 -0
  83. package/build-module/types/css-modules.d.js +1 -0
  84. package/build-module/types/css-modules.d.js.map +7 -0
  85. package/build-module/types.js +1 -0
  86. package/build-module/types.js.map +7 -0
  87. package/build-module/use-theme-provider-styles.js +200 -0
  88. package/build-module/use-theme-provider-styles.js.map +7 -0
  89. package/build-style/style.css +3 -0
  90. package/build-types/color-ramps/index.d.ts +44 -0
  91. package/build-types/color-ramps/index.d.ts.map +1 -0
  92. package/build-types/color-ramps/lib/cache-utils.d.ts +22 -0
  93. package/build-types/color-ramps/lib/cache-utils.d.ts.map +1 -0
  94. package/build-types/color-ramps/lib/constants.d.ts +38 -0
  95. package/build-types/color-ramps/lib/constants.d.ts.map +1 -0
  96. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts +37 -0
  97. package/build-types/color-ramps/lib/find-color-with-constraints.d.ts.map +1 -0
  98. package/build-types/color-ramps/lib/index.d.ts +11 -0
  99. package/build-types/color-ramps/lib/index.d.ts.map +1 -0
  100. package/build-types/color-ramps/lib/ramp-configs.d.ts +7 -0
  101. package/build-types/color-ramps/lib/ramp-configs.d.ts.map +1 -0
  102. package/build-types/color-ramps/lib/taper-chroma.d.ts +32 -0
  103. package/build-types/color-ramps/lib/taper-chroma.d.ts.map +1 -0
  104. package/build-types/color-ramps/lib/types.d.ts +78 -0
  105. package/build-types/color-ramps/lib/types.d.ts.map +1 -0
  106. package/build-types/color-ramps/lib/utils.d.ts +38 -0
  107. package/build-types/color-ramps/lib/utils.d.ts.map +1 -0
  108. package/build-types/color-ramps/stories/index.story.d.ts +14 -0
  109. package/build-types/color-ramps/stories/index.story.d.ts.map +1 -0
  110. package/build-types/color-ramps/stories/ramp-table.d.ts +19 -0
  111. package/build-types/color-ramps/stories/ramp-table.d.ts.map +1 -0
  112. package/build-types/context.d.ts +10 -0
  113. package/build-types/context.d.ts.map +1 -0
  114. package/build-types/index.d.ts +2 -0
  115. package/build-types/index.d.ts.map +1 -0
  116. package/build-types/lock-unlock.d.ts +2 -0
  117. package/build-types/lock-unlock.d.ts.map +1 -0
  118. package/build-types/prebuilt/js/design-tokens.d.ts +3 -0
  119. package/build-types/prebuilt/js/design-tokens.d.ts.map +1 -0
  120. package/build-types/prebuilt/ts/design-tokens.d.ts +7 -0
  121. package/build-types/prebuilt/ts/design-tokens.d.ts.map +1 -0
  122. package/build-types/private-apis.d.ts +2 -0
  123. package/build-types/private-apis.d.ts.map +1 -0
  124. package/build-types/stories/index.story.d.ts +15 -0
  125. package/build-types/stories/index.story.d.ts.map +1 -0
  126. package/build-types/theme-provider.d.ts +3 -0
  127. package/build-types/theme-provider.d.ts.map +1 -0
  128. package/build-types/types.d.ts +42 -0
  129. package/build-types/types.d.ts.map +1 -0
  130. package/build-types/use-theme-provider-styles.d.ts +17 -0
  131. package/build-types/use-theme-provider-styles.d.ts.map +1 -0
  132. package/docs/ds-tokens.md +283 -0
  133. package/package.json +58 -0
  134. package/src/color-ramps/index.ts +155 -0
  135. package/src/color-ramps/lib/cache-utils.ts +56 -0
  136. package/src/color-ramps/lib/constants.ts +85 -0
  137. package/src/color-ramps/lib/find-color-with-constraints.ts +190 -0
  138. package/src/color-ramps/lib/index.ts +369 -0
  139. package/src/color-ramps/lib/ramp-configs.ts +309 -0
  140. package/src/color-ramps/lib/taper-chroma.ts +226 -0
  141. package/src/color-ramps/lib/types.ts +90 -0
  142. package/src/color-ramps/lib/utils.ts +161 -0
  143. package/src/color-ramps/stories/index.story.tsx +264 -0
  144. package/src/color-ramps/stories/ramp-table.tsx +212 -0
  145. package/src/color-ramps/test/__snapshots__/index.test.ts.snap +1280 -0
  146. package/src/color-ramps/test/index.test.ts +94 -0
  147. package/src/context.ts +19 -0
  148. package/src/index.ts +2 -0
  149. package/src/lock-unlock.ts +10 -0
  150. package/src/prebuilt/css/design-tokens.css +401 -0
  151. package/src/prebuilt/js/design-tokens.js +116 -0
  152. package/src/prebuilt/json/figma.json +1317 -0
  153. package/src/prebuilt/ts/design-tokens.ts +335 -0
  154. package/src/private-apis.ts +12 -0
  155. package/src/stories/index.story.tsx +426 -0
  156. package/src/style.module.css +3 -0
  157. package/src/theme-provider.tsx +87 -0
  158. package/src/types/css-modules.d.ts +4 -0
  159. package/src/types.ts +44 -0
  160. package/src/use-theme-provider-styles.ts +247 -0
  161. package/terrazzo.config.ts +102 -0
  162. package/tokens/border.json +34 -0
  163. package/tokens/color.json +877 -0
  164. package/tokens/elevation.json +201 -0
  165. package/tokens/spacing.json +45 -0
  166. package/tokens/typography.json +93 -0
  167. package/tsconfig.json +9 -0
  168. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { RampStepConfig, RampConfig, RampDirection } from './types';
5
+ import type { TaperChromaOptions } from './taper-chroma';
6
+
7
+ const lightnessConstraintForegroundHighContrast = (
8
+ direction: RampDirection
9
+ ) =>
10
+ direction === 'lighter'
11
+ ? 0.9551 // lightness of #f0f0f0 (ie $gray-100)
12
+ : 0.235; // lightness of #1e1e1e (ie $gray-900)
13
+ const lightnessConstraintForegroundMediumContrast = (
14
+ direction: RampDirection
15
+ ) =>
16
+ direction === 'lighter'
17
+ ? 0.77 // lightness of #b4b4b4
18
+ : 0.56; // lightness of #747474
19
+ const lightnessConstraintBgFill = ( direction: RampDirection ) =>
20
+ direction === 'lighter'
21
+ ? 0.67 // lightness of #969696 (7:1 vs black)
22
+ : 0.45; // lightness of #555555 (7:1 vs white)
23
+
24
+ const BG_SURFACE_TAPER_CHROMA: TaperChromaOptions = {
25
+ alpha: 0.7,
26
+ };
27
+ const FG_TAPER_CHROMA: TaperChromaOptions = {
28
+ alpha: 0.6,
29
+ kLight: 0.2,
30
+ kDark: 0.2,
31
+ };
32
+ const STROKE_TAPER_CHROMA: TaperChromaOptions = {
33
+ alpha: 0.6,
34
+ radiusDark: 0.01,
35
+ radiusLight: 0.01,
36
+ kLight: 0.8,
37
+ kDark: 0.8,
38
+ };
39
+ const ACCENT_SURFACE_TAPER_CHROMA: TaperChromaOptions = {
40
+ alpha: 0.75,
41
+ radiusDark: 0.01,
42
+ radiusLight: 0.01,
43
+ };
44
+
45
+ const fgSurface4Config: RampStepConfig = {
46
+ contrast: {
47
+ reference: 'surface3',
48
+ followDirection: 'main',
49
+ target: 7,
50
+ preferLighter: true,
51
+ },
52
+ lightness: lightnessConstraintForegroundHighContrast,
53
+ taperChromaOptions: FG_TAPER_CHROMA,
54
+ };
55
+
56
+ export const BG_RAMP_CONFIG: RampConfig = {
57
+ // Surface
58
+ surface1: {
59
+ contrast: {
60
+ reference: 'surface2',
61
+ followDirection: 'opposite',
62
+ target: 1.02,
63
+ ignoreWhenAdjustingSeed: true,
64
+ },
65
+ taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
66
+ },
67
+ surface2: {
68
+ contrast: {
69
+ reference: 'seed',
70
+ followDirection: 'main',
71
+ target: 1,
72
+ },
73
+ },
74
+ surface3: {
75
+ contrast: {
76
+ reference: 'surface2',
77
+ followDirection: 'main',
78
+ target: 1.02,
79
+ },
80
+ taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
81
+ },
82
+ surface4: {
83
+ contrast: {
84
+ reference: 'surface2',
85
+ followDirection: 'main',
86
+ target: 1.08,
87
+ },
88
+ taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
89
+ },
90
+ surface5: {
91
+ contrast: {
92
+ reference: 'surface2',
93
+ followDirection: 'main',
94
+ target: 1.2,
95
+ },
96
+ taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
97
+ },
98
+ surface6: {
99
+ contrast: {
100
+ reference: 'surface2',
101
+ followDirection: 'main',
102
+ target: 1.4,
103
+ },
104
+ taperChromaOptions: BG_SURFACE_TAPER_CHROMA,
105
+ },
106
+ // Bg fill
107
+ bgFill1: {
108
+ contrast: {
109
+ reference: 'surface2',
110
+ followDirection: 'main',
111
+ target: 4,
112
+ },
113
+ lightness: lightnessConstraintBgFill,
114
+ },
115
+ bgFill2: {
116
+ contrast: {
117
+ reference: 'bgFill1',
118
+ followDirection: 'main',
119
+ target: 1.2,
120
+ },
121
+ },
122
+ bgFillInverted1: {
123
+ contrast: {
124
+ reference: 'bgFillInverted2',
125
+ followDirection: 'opposite',
126
+ target: 1.2,
127
+ },
128
+ },
129
+ bgFillInverted2: fgSurface4Config,
130
+ bgFillDark: {
131
+ contrast: {
132
+ reference: 'surface3',
133
+ followDirection: 'darker', // This is what causes the token to be always dark
134
+ target: 7,
135
+ ignoreWhenAdjustingSeed: true,
136
+ },
137
+ lightness: lightnessConstraintForegroundHighContrast,
138
+ taperChromaOptions: FG_TAPER_CHROMA,
139
+ },
140
+ // Stroke
141
+ stroke1: {
142
+ contrast: {
143
+ reference: 'stroke3',
144
+ followDirection: 'opposite',
145
+ target: 2.2,
146
+ },
147
+ taperChromaOptions: STROKE_TAPER_CHROMA,
148
+ },
149
+ stroke2: {
150
+ contrast: {
151
+ reference: 'stroke3',
152
+ followDirection: 'opposite',
153
+ target: 1.5,
154
+ },
155
+ taperChromaOptions: STROKE_TAPER_CHROMA,
156
+ },
157
+ stroke3: {
158
+ contrast: {
159
+ reference: 'surface3',
160
+ followDirection: 'main',
161
+ target: 3,
162
+ },
163
+ taperChromaOptions: STROKE_TAPER_CHROMA,
164
+ },
165
+ stroke4: {
166
+ contrast: {
167
+ reference: 'stroke3',
168
+ followDirection: 'main',
169
+ target: 1.5,
170
+ },
171
+ taperChromaOptions: STROKE_TAPER_CHROMA,
172
+ },
173
+ // fgSurface
174
+ fgSurface1: {
175
+ contrast: {
176
+ reference: 'surface3',
177
+ followDirection: 'main',
178
+ target: 2,
179
+ preferLighter: true,
180
+ },
181
+ taperChromaOptions: FG_TAPER_CHROMA,
182
+ },
183
+ fgSurface2: {
184
+ contrast: {
185
+ reference: 'surface3',
186
+ followDirection: 'main',
187
+ target: 3,
188
+ preferLighter: true,
189
+ },
190
+ taperChromaOptions: FG_TAPER_CHROMA,
191
+ },
192
+ fgSurface3: {
193
+ contrast: {
194
+ reference: 'surface3',
195
+ followDirection: 'main',
196
+ target: 4.5,
197
+ preferLighter: true,
198
+ },
199
+ lightness: lightnessConstraintForegroundMediumContrast,
200
+ taperChromaOptions: FG_TAPER_CHROMA,
201
+ },
202
+ fgSurface4: fgSurface4Config,
203
+ // fgFill
204
+ fgFill: {
205
+ contrast: {
206
+ reference: 'bgFill1',
207
+ followDirection: 'best',
208
+ target: 4.5,
209
+ preferLighter: true,
210
+ },
211
+ lightness: lightnessConstraintForegroundHighContrast,
212
+ taperChromaOptions: FG_TAPER_CHROMA,
213
+ },
214
+ fgFillInverted: {
215
+ contrast: {
216
+ reference: 'bgFillInverted1',
217
+ followDirection: 'best',
218
+ target: 4.5,
219
+ preferLighter: true,
220
+ },
221
+ lightness: lightnessConstraintForegroundHighContrast,
222
+ taperChromaOptions: FG_TAPER_CHROMA,
223
+ },
224
+ fgFillDark: {
225
+ contrast: {
226
+ reference: 'bgFillDark',
227
+ followDirection: 'best',
228
+ target: 4.5,
229
+ preferLighter: true,
230
+ },
231
+ lightness: lightnessConstraintForegroundHighContrast,
232
+ taperChromaOptions: FG_TAPER_CHROMA,
233
+ },
234
+ };
235
+
236
+ // BG_RAMP: seed => surface2 => {bgFill, surface3 => all other tokens}
237
+ // ACCENT_RAMP: seed => bgFill1 => surface2 => surface3 => all other tokens
238
+ export const ACCENT_RAMP_CONFIG: RampConfig = {
239
+ ...BG_RAMP_CONFIG,
240
+ surface1: {
241
+ ...BG_RAMP_CONFIG.surface1,
242
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
243
+ },
244
+ surface2: {
245
+ contrast: {
246
+ reference: 'bgFill1',
247
+ followDirection: 'opposite',
248
+ target: BG_RAMP_CONFIG.bgFill1.contrast.target,
249
+ ignoreWhenAdjustingSeed: true,
250
+ },
251
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
252
+ },
253
+ surface3: {
254
+ ...BG_RAMP_CONFIG.surface3,
255
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
256
+ },
257
+ surface4: {
258
+ ...BG_RAMP_CONFIG.surface4,
259
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
260
+ },
261
+ surface5: {
262
+ ...BG_RAMP_CONFIG.surface5,
263
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
264
+ },
265
+ surface6: {
266
+ ...BG_RAMP_CONFIG.surface6,
267
+ taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA,
268
+ },
269
+ bgFill1: {
270
+ contrast: {
271
+ reference: 'seed',
272
+ followDirection: 'main',
273
+ target: 1,
274
+ },
275
+ },
276
+ stroke1: {
277
+ ...BG_RAMP_CONFIG.stroke1,
278
+ },
279
+ stroke2: {
280
+ ...BG_RAMP_CONFIG.stroke2,
281
+ },
282
+ stroke3: {
283
+ ...BG_RAMP_CONFIG.stroke3,
284
+ sameAsIfPossible: 'fgSurface3',
285
+ taperChromaOptions: undefined,
286
+ },
287
+ stroke4: {
288
+ ...BG_RAMP_CONFIG.stroke4,
289
+ taperChromaOptions: undefined,
290
+ },
291
+ // fgSurface: do not de-saturate
292
+ fgSurface1: {
293
+ ...BG_RAMP_CONFIG.fgSurface1,
294
+ taperChromaOptions: undefined,
295
+ },
296
+ fgSurface2: {
297
+ ...BG_RAMP_CONFIG.fgSurface2,
298
+ taperChromaOptions: undefined,
299
+ },
300
+ fgSurface3: {
301
+ ...BG_RAMP_CONFIG.fgSurface3,
302
+ taperChromaOptions: undefined,
303
+ sameAsIfPossible: 'bgFill1',
304
+ },
305
+ fgSurface4: {
306
+ ...BG_RAMP_CONFIG.fgSurface4,
307
+ taperChromaOptions: undefined,
308
+ },
309
+ };
@@ -0,0 +1,226 @@
1
+ // npm i colorjs.io
2
+ /**
3
+ * External dependencies
4
+ */
5
+ import Color from 'colorjs.io';
6
+
7
+ export interface TaperChromaOptions {
8
+ gamut?: 'p3' | 'srgb'; // target gamut (default "p3")
9
+ alpha?: number; // base fraction of Cmax at target (default 0.62)
10
+ carry?: number; // seed vividness carry exponent β in [0..1] (default 0.5)
11
+ cUpperBound?: number; // hard search cap for C (default 0.45)
12
+ // Continuous taper around the seed (desaturate both sides slightly)
13
+ radiusLight?: number; // distance in L where kLight is reached (default 0.20)
14
+ radiusDark?: number; // distance in L where kDark is reached (default 0.20)
15
+ kLight?: number; // floor multiplier near lighter side (default 0.85)
16
+ kDark?: number; // floor multiplier near darker side (default 0.85)
17
+ // Achromatic handling
18
+ hueFallback?: number; // degrees: if seed is achromatic and you still want color
19
+ achromaEpsilon?: number; // ≤ this chroma → treat as achromatic (default 0.005)
20
+ }
21
+
22
+ /**
23
+ * Given the seed and the target lightness, tapers the chroma smoothly.
24
+ * - C_intended = Cmax(Lt,H0) * alpha * (seedRelative^carry)
25
+ * - Continuous taper vs |Lt - Ls| to softly reduce chroma for neighbors
26
+ * - Downward-only clamp on C (preserve L & H)
27
+ * @param seed
28
+ * @param lTarget
29
+ * @param options
30
+ */
31
+ export function taperChroma(
32
+ seed: Color, // already OKLCH
33
+ lTarget: number, // [0..1]
34
+ options: TaperChromaOptions = {}
35
+ ): { l: number; c: number } {
36
+ const gamut = options.gamut ?? 'p3';
37
+ const alpha = options.alpha ?? 0.65; // 0.7-0.8 works well for accent surface
38
+ const carry = options.carry ?? 0.5;
39
+ const cUpperBound = options.cUpperBound ?? 0.45;
40
+ const radiusLight = options.radiusLight ?? 0.2;
41
+ const radiusDark = options.radiusDark ?? 0.2;
42
+ const kLight = options.kLight ?? 0.85;
43
+ const kDark = options.kDark ?? 0.85;
44
+ const achromaEpsilon = options.achromaEpsilon ?? 0.005;
45
+
46
+ const cSeed = Math.max( 0, seed.oklch.c );
47
+ let hSeed = Number( seed.oklch.h );
48
+
49
+ const chromaIsTiny = cSeed < achromaEpsilon;
50
+ const hueIsInvalid = ! Number.isFinite( hSeed );
51
+
52
+ if ( chromaIsTiny || hueIsInvalid ) {
53
+ if ( typeof options.hueFallback === 'number' ) {
54
+ hSeed = normalizeHue( options.hueFallback );
55
+ } else {
56
+ // Respect achromatic intent: grayscale at target L
57
+ return new Color( 'oklch', [ clamp01( lTarget ), 0, 0 ] );
58
+ }
59
+ }
60
+
61
+ // Capacity at seed and target
62
+ const lSeed = clamp01( seed.oklch.l );
63
+ const cmaxSeed = getCachedMaxChromaAtLH( lSeed, hSeed, gamut, cUpperBound );
64
+ const cmaxTarget = getCachedMaxChromaAtLH(
65
+ clamp01( lTarget ),
66
+ hSeed,
67
+ gamut,
68
+ cUpperBound
69
+ );
70
+
71
+ // Seed vividness ratio (hue-fair normalization)
72
+ let seedRelative = 0;
73
+ const denom = cmaxSeed > 0 ? cmaxSeed : 1e-6;
74
+ seedRelative = clamp01( cSeed / denom );
75
+
76
+ // Intended chroma from local capacity, tempered by seed vividness
77
+ const cIntendedBase = alpha * cmaxTarget;
78
+ const cWithCarry =
79
+ cIntendedBase * Math.pow( seedRelative, clamp01( carry ) );
80
+
81
+ // Gentle, symmetric desaturation vs distance in L
82
+ const t = continuousTaper( lSeed, lTarget, {
83
+ radiusLight,
84
+ radiusDark,
85
+ kLight,
86
+ kDark,
87
+ } );
88
+ let cPlanned = cWithCarry * t;
89
+
90
+ // Downward-only clamp (preserve L & H)
91
+ const lOut = clamp01( lTarget );
92
+ const candidate = new Color( 'oklch', [ lOut, cPlanned, hSeed ] );
93
+ if ( ! candidate.inGamut( gamut ) ) {
94
+ const cap = Math.min( cPlanned, cUpperBound );
95
+ cPlanned = getCachedMaxChromaAtLH( lOut, hSeed, gamut, cap );
96
+ }
97
+
98
+ cPlanned = Math.min( cPlanned, cSeed );
99
+
100
+ return { l: lOut, c: cPlanned };
101
+ }
102
+
103
+ /* ---------------- helpers & caches ---------------- */
104
+
105
+ function clamp01( x: number ): number {
106
+ if ( x < 0 ) {
107
+ return 0;
108
+ }
109
+ if ( x > 1 ) {
110
+ return 1;
111
+ }
112
+ return x;
113
+ }
114
+ function normalizeHue( h: number ): number {
115
+ let hue = h % 360;
116
+ if ( hue < 0 ) {
117
+ hue += 360;
118
+ }
119
+ return hue;
120
+ }
121
+ function raisedCosine( u: number ): number {
122
+ const x = clamp01( u );
123
+ return 0.5 - 0.5 * Math.cos( Math.PI * x );
124
+ }
125
+
126
+ /**
127
+ * smooth, distance-from-seed chroma taper (raised-cosine per side)
128
+ * @param seedL
129
+ * @param targetL
130
+ * @param opts
131
+ * @param opts.radiusLight
132
+ * @param opts.radiusDark
133
+ * @param opts.kLight
134
+ * @param opts.kDark
135
+ */
136
+ function continuousTaper(
137
+ seedL: number,
138
+ targetL: number,
139
+ opts: {
140
+ radiusLight: number;
141
+ radiusDark: number;
142
+ kLight: number;
143
+ kDark: number;
144
+ }
145
+ ): number {
146
+ const d = targetL - seedL;
147
+ if ( d >= 0 ) {
148
+ const u = opts.radiusLight > 0 ? Math.abs( d ) / opts.radiusLight : 1;
149
+ const w = raisedCosine( u > 1 ? 1 : u );
150
+ return 1 - ( 1 - opts.kLight ) * w;
151
+ }
152
+ const u = opts.radiusDark > 0 ? Math.abs( d ) / opts.radiusDark : 1;
153
+ const w = raisedCosine( u > 1 ? 1 : u );
154
+ return 1 - ( 1 - opts.kDark ) * w;
155
+ }
156
+
157
+ /* ---- chroma-capacity queries with small caches ---- */
158
+
159
+ const maxChromaCache = new Map< string, number >();
160
+
161
+ function keyMax(
162
+ l: number,
163
+ h: number,
164
+ gamut: 'p3' | 'srgb',
165
+ cap: number
166
+ ): string {
167
+ // Quantize to keep cache compact
168
+ const lq = quantize( l, 1e-3 );
169
+ const hq = quantize( normalizeHue( h ), 1e-1 );
170
+ const cq = quantize( cap, 1e-3 );
171
+ return `${ gamut }|L:${ lq }|H:${ hq }|cap:${ cq }`;
172
+ }
173
+ function quantize( x: number, step: number ): number {
174
+ const k = Math.round( x / step );
175
+ return k * step;
176
+ }
177
+
178
+ function getCachedMaxChromaAtLH(
179
+ l: number,
180
+ h: number,
181
+ gamut: 'p3' | 'srgb',
182
+ cap: number
183
+ ): number {
184
+ const key = keyMax( l, h, gamut, cap );
185
+ const hit = maxChromaCache.get( key );
186
+ if ( typeof hit === 'number' ) {
187
+ return hit;
188
+ }
189
+
190
+ const computed = maxInGamutChromaAtLH( l, h, gamut, cap );
191
+ maxChromaCache.set( key, computed );
192
+ return computed;
193
+ }
194
+
195
+ /**
196
+ * Binary-search the max in-gamut chroma at fixed (L,H) in the target gamut
197
+ * @param l
198
+ * @param h
199
+ * @param gamut
200
+ * @param cap
201
+ */
202
+ function maxInGamutChromaAtLH(
203
+ l: number,
204
+ h: number,
205
+ gamut: 'p3' | 'srgb',
206
+ cap: number
207
+ ): number {
208
+ let lo = 0;
209
+ let hi = cap;
210
+ let ok = 0;
211
+
212
+ const lFixed = clamp01( l );
213
+ const hFixed = normalizeHue( h );
214
+
215
+ for ( let i = 0; i < 18; i++ ) {
216
+ const mid = ( lo + hi ) / 2;
217
+ const probe = new Color( 'oklch', [ lFixed, mid, hFixed ] );
218
+ if ( probe.inGamut( gamut ) ) {
219
+ ok = mid;
220
+ lo = mid;
221
+ } else {
222
+ hi = mid;
223
+ }
224
+ }
225
+ return ok;
226
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { TaperChromaOptions } from './taper-chroma';
5
+
6
+ export type Ramp = {
7
+ // Backgrounds for surfaces (nuanced, slight variations compared to bg)
8
+ surface1: string;
9
+ surface2: string;
10
+ surface3: string;
11
+ surface4: string;
12
+ surface5: string;
13
+ surface6: string;
14
+ // Strokes
15
+ stroke1: string;
16
+ stroke2: string;
17
+ stroke3: string;
18
+ stroke4: string;
19
+ // Stronger backgrounds for primary UI elements
20
+ bgFill1: string;
21
+ bgFill2: string;
22
+ bgFillInverted1: string;
23
+ bgFillInverted2: string;
24
+ bgFillDark: string;
25
+ // Foreground (text, icon) colors
26
+ fgSurface1: string;
27
+ fgSurface2: string;
28
+ fgSurface3: string;
29
+ fgSurface4: string;
30
+ // Foreground (text, icon) colors on top of bgFill
31
+ fgFill: string;
32
+ fgFillInverted: string;
33
+ fgFillDark: string;
34
+ };
35
+
36
+ export type RampDirection = 'lighter' | 'darker';
37
+ export type FollowDirection = 'main' | 'opposite' | 'best' | RampDirection;
38
+ export type ContrastRequirement = {
39
+ /** The reference color against which to calculate the contrast */
40
+ reference: keyof Ramp | 'seed';
41
+ /**
42
+ * Which direction should the algorithm search a matching color in:
43
+ * - main: follow the same direction as the ramp's main direction
44
+ * - opposite: follow the opposite direction of the ramp
45
+ * - best: pick the direction that has the most contrast headroom
46
+ * - hardcoded ramp direction (useful for generating colors that always
47
+ * light/dark regardless of the ramp direction)
48
+ */
49
+ followDirection: FollowDirection;
50
+ /**
51
+ * Prefer "lighter" direction when searching for a contrasting color.
52
+ * Especially useful for foreground color to counter the poor results that the
53
+ * WCAG algo gives when contrasting white text over mid-lightness backgrounds.
54
+ */
55
+ preferLighter?: boolean;
56
+ /**
57
+ * The contrast target to meet.
58
+ */
59
+ target: number;
60
+ /**
61
+ * When true, the algorithm won't count a failure in meeting the contrast
62
+ * target as a reason to recalculate the ramp.
63
+ */
64
+ ignoreWhenAdjustingSeed?: boolean;
65
+ };
66
+
67
+ export type RampStepConfig = {
68
+ contrast: ContrastRequirement;
69
+ lightness?: ( direction: RampDirection ) => number;
70
+ taperChromaOptions?: TaperChromaOptions;
71
+ /**
72
+ * If specified, try to reuse the color from this step if it meets
73
+ * the contrast requirements. This reduces the number of unique colors
74
+ * in the ramp and improves consistency.
75
+ */
76
+ sameAsIfPossible?: keyof Ramp;
77
+ };
78
+
79
+ export type RampConfig = Record< keyof Ramp, RampStepConfig >;
80
+
81
+ export type RampResult = {
82
+ ramp: Record<
83
+ keyof Ramp,
84
+ {
85
+ color: string;
86
+ warning: boolean;
87
+ }
88
+ >;
89
+ direction: RampDirection;
90
+ };