@fibery/ui-kit 1.40.3 → 1.41.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 (174) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/eslint.config.mjs +16 -0
  3. package/package.json +11 -13
  4. package/src/a11y-color.test.ts +162 -0
  5. package/src/a11y-color.ts +13 -20
  6. package/src/actions-menu/actions-menu-item.tsx +9 -12
  7. package/src/actions-menu/context-actions-menu.tsx +8 -3
  8. package/src/ai/model.tsx +0 -1
  9. package/src/ai/temperature.tsx +0 -1
  10. package/src/animated-height-container.tsx +3 -3
  11. package/src/antd/ant-modal.tsx +10 -5
  12. package/src/antd/index.tsx +3 -0
  13. package/src/antd/input.tsx +0 -1
  14. package/src/antd/styles.ts +15 -15
  15. package/src/app-icon-with-fallback.tsx +6 -6
  16. package/src/app-icon-wrapper.tsx +2 -3
  17. package/src/app-icon.tsx +4 -4
  18. package/src/avatar.tsx +3 -3
  19. package/src/button/base-button.tsx +0 -1
  20. package/src/button/button.tsx +1 -0
  21. package/src/button/make-button-colors.ts +5 -13
  22. package/src/checkbox.tsx +2 -2
  23. package/src/collapsible-section.tsx +11 -1
  24. package/src/color-picker/ColorPickerOrLoader.js +2 -2
  25. package/src/color-utils.test.ts +307 -0
  26. package/src/color-utils.ts +215 -0
  27. package/src/command-menu/index.tsx +1 -0
  28. package/src/comment.tsx +3 -2
  29. package/src/context-menu/index.tsx +12 -7
  30. package/src/date-picker/contexts.ts +2 -2
  31. package/src/date-picker/date-range-picker.tsx +3 -4
  32. package/src/date-picker/single-date-picker.tsx +147 -119
  33. package/src/date-picker/types.ts +4 -3
  34. package/src/day-select/iso-week-day-select.tsx +2 -2
  35. package/src/day-select/week-day-select.tsx +2 -2
  36. package/src/delayed.tsx +0 -1
  37. package/src/design-system/alpha.test.ts +59 -0
  38. package/src/design-system/alpha.ts +41 -0
  39. package/src/design-system/animation.ts +10 -0
  40. package/src/design-system/colors-css.test.ts +39 -0
  41. package/src/design-system/colors-css.ts +25 -0
  42. package/src/design-system/colors-js.test.ts +232 -0
  43. package/src/design-system/colors-js.ts +107 -0
  44. package/src/design-system/colors.test.ts +74 -0
  45. package/src/design-system/colors.ts +759 -0
  46. package/src/design-system/date.ts +3 -0
  47. package/src/design-system/layout.ts +106 -0
  48. package/src/design-system/theme.test.ts +94 -0
  49. package/src/design-system/theme.ts +93 -0
  50. package/src/design-system/typography.ts +179 -0
  51. package/src/design-system/vars.test.ts +1679 -0
  52. package/src/design-system/vars.ts +52 -0
  53. package/src/design-system.test.ts +45 -22
  54. package/src/design-system.ts +25 -1339
  55. package/src/dropdown-menu/index.tsx +21 -16
  56. package/src/emoji-picker/app-icon-picker.tsx +5 -5
  57. package/src/emoji-picker/emoji-picker-content-with-color.tsx +4 -4
  58. package/src/emoji-picker/icon-emoji-picker.tsx +3 -3
  59. package/src/favorites-icon.tsx +1 -1
  60. package/src/fibermoji-placeholder.tsx +3 -3
  61. package/src/field-container.tsx +11 -2
  62. package/src/file-item/file-icon.tsx +169 -0
  63. package/src/file-item/file-menu-items.tsx +68 -0
  64. package/src/file-item/file-preview-actions.tsx +38 -0
  65. package/src/file-item/file-title.tsx +48 -0
  66. package/src/file-item/types.ts +27 -0
  67. package/src/file-item/use-register-in-image-gallery.tsx +70 -0
  68. package/src/file-item-2.tsx +32 -348
  69. package/src/file-item.tsx +6 -3
  70. package/src/hue-shift.test.ts +91 -0
  71. package/src/icons/ast/ChatBubble.ts +8 -0
  72. package/src/icons/ast/ChatFloat.ts +8 -0
  73. package/src/icons/ast/ChatSidebar.ts +8 -0
  74. package/src/icons/ast/FileCounter.ts +8 -0
  75. package/src/icons/ast/FileMultiple.ts +8 -0
  76. package/src/icons/ast/FileOther.ts +8 -0
  77. package/src/icons/ast/NoBorder.ts +8 -0
  78. package/src/icons/ast/NoFill.ts +8 -0
  79. package/src/icons/ast/ValueEdit.ts +8 -0
  80. package/src/icons/ast/index.tsx +9 -0
  81. package/src/icons/react/ChatBubble.tsx +13 -0
  82. package/src/icons/react/ChatFloat.tsx +13 -0
  83. package/src/icons/react/ChatSidebar.tsx +13 -0
  84. package/src/icons/react/FileCounter.tsx +13 -0
  85. package/src/icons/react/FileMultiple.tsx +13 -0
  86. package/src/icons/react/FileOther.tsx +13 -0
  87. package/src/icons/react/NoBorder.tsx +13 -0
  88. package/src/icons/react/NoFill.tsx +13 -0
  89. package/src/icons/react/ValueEdit.tsx +13 -0
  90. package/src/icons/react/index.tsx +9 -0
  91. package/src/icons/svg/chat-bubble.svg +4 -0
  92. package/src/icons/svg/chat-float.svg +4 -0
  93. package/src/icons/svg/chat-sidebar.svg +4 -0
  94. package/src/icons/svg/file-counter.svg +3 -0
  95. package/src/icons/svg/file-multiple.svg +3 -0
  96. package/src/icons/svg/file-other.svg +3 -0
  97. package/src/icons/svg/no-border.svg +17 -0
  98. package/src/icons/svg/no-fill.svg +4 -0
  99. package/src/icons/svg/value-edit.svg +3 -0
  100. package/src/images-gallery/images-gallery.tsx +8 -6
  101. package/src/images-gallery/slide-buttons.tsx +4 -11
  102. package/src/is-iOS.ts +0 -1
  103. package/src/is-in-popup.ts +2 -1
  104. package/src/lists/actions-menu-row-surface.tsx +7 -7
  105. package/src/loading-sausage.tsx +2 -2
  106. package/src/media-query-utils.ts +1 -2
  107. package/src/mobile-keyboard-aware-popup.tsx +6 -5
  108. package/src/modal-menu/modal-menu-content.tsx +2 -2
  109. package/src/number-input/decimal.js +9 -7
  110. package/src/number-input/{number-inline-input-with-autosize.tsx → number-input-inline-with-autosize.tsx} +20 -41
  111. package/src/number-input/number-input-inline.tsx +118 -0
  112. package/src/number-input/{index.test.js → number-input.test.tsx} +38 -43
  113. package/src/number-input/number-input.tsx +63 -121
  114. package/src/number-input/types.ts +19 -0
  115. package/src/number-input/utils.ts +61 -0
  116. package/src/online-users.tsx +2 -2
  117. package/src/palette-generator.test.ts +309 -0
  118. package/src/palette-generator.ts +160 -0
  119. package/src/palettes/_.ts +72 -0
  120. package/src/palettes/slate-arch.colors-snapshot.test.ts +1689 -0
  121. package/src/palettes/slate-arch.ts +188 -0
  122. package/src/palettes/slate-user.colors-snapshot.test.ts +1689 -0
  123. package/src/palettes/slate-user.ts +187 -0
  124. package/src/palettes/warm-arch.colors-snapshot.test.ts +1689 -0
  125. package/src/palettes/warm-arch.ts +222 -0
  126. package/src/palettes/warm-user.colors-snapshot.test.ts +1689 -0
  127. package/src/palettes/warm-user.ts +222 -0
  128. package/src/platform.ts +0 -3
  129. package/src/popover/get-element-ref.ts +28 -0
  130. package/src/popover/index.tsx +236 -339
  131. package/src/popover/{mobile-popup-context.tsx → mobile-popover-context.tsx} +7 -7
  132. package/src/popover/mobile-popover.tsx +169 -0
  133. package/src/popover/modifiers.tsx +2 -2
  134. package/src/popover/popup-stack-context.tsx +8 -9
  135. package/src/progress.tsx +2 -2
  136. package/src/reactions/reaction-button.tsx +12 -6
  137. package/src/root-theme-provider.test.tsx +411 -0
  138. package/src/scale-generator.ts +356 -0
  139. package/src/select/components/menu-list-virtualized.tsx +12 -25
  140. package/src/select/components/menu.tsx +12 -2
  141. package/src/select/index.tsx +6 -5
  142. package/src/select/select.tsx +38 -39
  143. package/src/select/styles.ts +0 -1
  144. package/src/select/util.ts +1 -1
  145. package/src/static-palettes.ts +356 -0
  146. package/src/thematic-color-picker.tsx +266 -0
  147. package/src/thematic-constants.tsx +27 -0
  148. package/src/thematic-controls.tsx +144 -0
  149. package/src/thematic-scales.tsx +122 -0
  150. package/src/thematic-state.ts +370 -0
  151. package/src/thematic.tsx +386 -0
  152. package/src/theme-provider.test.tsx +820 -0
  153. package/src/theme-provider.tsx +158 -92
  154. package/src/theme-settings.ts +67 -12
  155. package/src/theme-styles.ts +58 -6
  156. package/src/toast/toast-action.tsx +1 -1
  157. package/src/toast/toast.tsx +1 -2
  158. package/src/toggle-on-off.tsx +2 -2
  159. package/src/toggle.tsx +7 -8
  160. package/src/tooltip.tsx +14 -10
  161. package/src/type-badge.tsx +7 -14
  162. package/src/unit/styles.ts +2 -25
  163. package/src/unit/unit-with-tooltip.tsx +3 -2
  164. package/src/use-is-phone.tsx +7 -2
  165. package/src/use-long-press.tsx +2 -2
  166. package/src/use-on-screen-keyboard-data.tsx +2 -2
  167. package/src/with-data.tsx +4 -3
  168. package/src/workflow-progress-icon.tsx +2 -2
  169. package/.eslintignore +0 -3
  170. package/.eslintrc +0 -14
  171. package/src/__mocks__/createInlineTheme.js +0 -3
  172. package/src/create-inline-theme.ts +0 -66
  173. package/src/number-input/index.js +0 -191
  174. package/src/palette.ts +0 -237
@@ -0,0 +1,309 @@
1
+ import {getAlpha, OKLCH, parse, to} from "./color-utils";
2
+ import {type ColorScale, makeDefaultPalette, makeThematicPalette} from "./palette-generator";
3
+ import {zeroBasedFromOneBased} from "./scale-generator";
4
+
5
+ function roundToThreeDigits(number: number) {
6
+ return Math.round(number * 1000) / 1000;
7
+ }
8
+
9
+ function getOklch(color: string): {l: number; c: number; h: number} {
10
+ const oklch = to(parse(color), OKLCH);
11
+ return {
12
+ l: roundToThreeDigits(100 * (oklch.coords[0] ?? 0)),
13
+ c: oklch.coords[1] ?? 0,
14
+ h: oklch.coords[2] ?? 0,
15
+ };
16
+ }
17
+
18
+ function extractLightness(scale: ColorScale): number[] {
19
+ return zeroBasedFromOneBased(scale).map((color) => getOklch(color).l);
20
+ }
21
+
22
+ function extractChromaNormalized(scale: ColorScale): number[] {
23
+ const chromas = zeroBasedFromOneBased(scale).map((color) => getOklch(color).c);
24
+ const maxChroma = Math.max(...chromas);
25
+ return chromas.map((c) => roundToThreeDigits(c / maxChroma));
26
+ }
27
+
28
+ describe("palette-generator", () => {
29
+ describe("extract curves from default palette", () => {
30
+ it("extracts YELLOW curves", () => {
31
+ const palette = makeDefaultPalette();
32
+ expect(extractLightness(palette.yellow)).toMatchInlineSnapshot(`
33
+ [
34
+ 99.15,
35
+ 98.666,
36
+ 98.103,
37
+ 97.119,
38
+ 95.34,
39
+ 92.061,
40
+ 86.841,
41
+ 81.273,
42
+ 88.224,
43
+ 85.516,
44
+ 54.771,
45
+ 28.162,
46
+ ]
47
+ `);
48
+ expect(extractChromaNormalized(palette.yellow)).toMatchInlineSnapshot(`
49
+ [
50
+ 0.04,
51
+ 0.16,
52
+ 0.299,
53
+ 0.419,
54
+ 0.539,
55
+ 0.621,
56
+ 0.692,
57
+ 0.911,
58
+ 1,
59
+ 0.962,
60
+ 0.624,
61
+ 0.231,
62
+ ]
63
+ `);
64
+ expect(extractLightness(palette.yellowDark)).toMatchInlineSnapshot(`
65
+ [
66
+ 18.975,
67
+ 22.715,
68
+ 25.994,
69
+ 27.672,
70
+ 31.467,
71
+ 35.323,
72
+ 40.577,
73
+ 48.421,
74
+ 88.224,
75
+ 93.814,
76
+ 82.593,
77
+ 97.801,
78
+ ]
79
+ `);
80
+ expect(extractChromaNormalized(palette.yellowDark)).toMatchInlineSnapshot(`
81
+ [
82
+ 0.213,
83
+ 0.255,
84
+ 0.291,
85
+ 0.31,
86
+ 0.353,
87
+ 0.398,
88
+ 0.444,
89
+ 0.546,
90
+ 1,
91
+ 0.898,
92
+ 0.926,
93
+ 0.289,
94
+ ]
95
+ `);
96
+ // Extract hue from step 9 (primary yellow)
97
+ expect(getOklch(palette.yellow[9]).h).toMatchInlineSnapshot(`99.99954352835232`);
98
+ });
99
+
100
+ it("extracts SLATE curves", () => {
101
+ const palette = makeDefaultPalette();
102
+ expect(extractLightness(palette.base)).toMatchInlineSnapshot(`
103
+ [
104
+ 99.241,
105
+ 97.719,
106
+ 96.955,
107
+ 94.656,
108
+ 93.115,
109
+ 91.568,
110
+ 89.235,
111
+ 82.937,
112
+ 69.959,
113
+ 57.217,
114
+ 48.318,
115
+ 20.441,
116
+ ]
117
+ `);
118
+ expect(extractLightness(palette.baseDark)).toMatchInlineSnapshot(`
119
+ [
120
+ 20.441,
121
+ 22.667,
122
+ 25.908,
123
+ 28.012,
124
+ 30.078,
125
+ 32.109,
126
+ 36.079,
127
+ 43.714,
128
+ 54.586,
129
+ 59.818,
130
+ 70.787,
131
+ 94.656,
132
+ ]
133
+ `);
134
+ });
135
+
136
+ it("extracts INDIGO curves", () => {
137
+ const palette = makeDefaultPalette();
138
+ expect(extractLightness(palette.accent)).toMatchInlineSnapshot(`
139
+ [
140
+ 99.069,
141
+ 98.983,
142
+ 96.966,
143
+ 95.012,
144
+ 92.145,
145
+ 88.367,
146
+ 82.757,
147
+ 76.14,
148
+ 58.48,
149
+ 54.173,
150
+ 49.642,
151
+ 26.071,
152
+ ]
153
+ `);
154
+ expect(extractChromaNormalized(palette.accent)).toMatchInlineSnapshot(`
155
+ [
156
+ 0.018,
157
+ 0.027,
158
+ 0.08,
159
+ 0.126,
160
+ 0.19,
161
+ 0.27,
162
+ 0.384,
163
+ 0.529,
164
+ 0.934,
165
+ 1,
166
+ 0.878,
167
+ 0.44,
168
+ ]
169
+ `);
170
+ expect(extractLightness(palette.accentDark)).toMatchInlineSnapshot(`
171
+ [
172
+ 20.512,
173
+ 22.974,
174
+ 27.284,
175
+ 29.742,
176
+ 32.452,
177
+ 35.981,
178
+ 41.256,
179
+ 47.972,
180
+ 57.658,
181
+ 62.928,
182
+ 76.225,
183
+ 95.899,
184
+ ]
185
+ `);
186
+ expect(extractChromaNormalized(palette.accentDark)).toMatchInlineSnapshot(`
187
+ [
188
+ 0.119,
189
+ 0.226,
190
+ 0.357,
191
+ 0.444,
192
+ 0.475,
193
+ 0.517,
194
+ 0.72,
195
+ 0.952,
196
+ 1,
197
+ 0.921,
198
+ 0.601,
199
+ 0.111,
200
+ ]
201
+ `);
202
+ });
203
+ });
204
+
205
+ describe("white/black scales", () => {
206
+ it("generated neutral scales match default palette", () => {
207
+ const defaultPalette = makeDefaultPalette();
208
+ const generated = makeThematicPalette({
209
+ baseHue: 0,
210
+ baseChroma: 0,
211
+ accentHue: 264,
212
+ accentChroma: 0.16,
213
+ });
214
+
215
+ expect(zeroBasedFromOneBased(generated.whiteA)).toMatchInlineSnapshot(`
216
+ [
217
+ "rgba(255,255,255,0)",
218
+ "rgba(255,255,255,0.01)",
219
+ "rgba(255,255,255,0.03)",
220
+ "rgba(255,255,255,0.06)",
221
+ "rgba(255,255,255,0.09)",
222
+ "rgba(255,255,255,0.12)",
223
+ "rgba(255,255,255,0.18)",
224
+ "rgba(255,255,255,0.25)",
225
+ "rgba(255,255,255,0.39)",
226
+ "rgba(255,255,255,0.45)",
227
+ "rgba(255,255,255,0.59)",
228
+ "rgba(255,255,255,0.92)",
229
+ ]
230
+ `);
231
+ expect(zeroBasedFromOneBased(generated.blackA)).toMatchInlineSnapshot(`
232
+ [
233
+ "rgba(0,0,0,0.01)",
234
+ "rgba(0,0,0,0.03)",
235
+ "rgba(0,0,0,0.05)",
236
+ "rgba(0,0,0,0.07)",
237
+ "rgba(0,0,0,0.09)",
238
+ "rgba(0,0,0,0.11)",
239
+ "rgba(0,0,0,0.14)",
240
+ "rgba(0,0,0,0.22)",
241
+ "rgba(0,0,0,0.44)",
242
+ "rgba(0,0,0,0.48)",
243
+ "rgba(0,0,0,0.57)",
244
+ "rgba(0,0,0,0.91)",
245
+ ]
246
+ `);
247
+
248
+ const whiteADefault = zeroBasedFromOneBased(defaultPalette.whiteA);
249
+ const blackADefault = zeroBasedFromOneBased(defaultPalette.blackA);
250
+ const whiteAGenerated = zeroBasedFromOneBased(generated.whiteA);
251
+ const blackAGenerated = zeroBasedFromOneBased(generated.blackA);
252
+ // default palette uses hsla strings, generated uses rgba — compare parsed alpha values
253
+ for (let i = 0; i < 12; i++) {
254
+ const defWhiteAlpha = getAlpha(whiteADefault[i]);
255
+ const genWhiteAlpha = getAlpha(whiteAGenerated[i]);
256
+ expect(genWhiteAlpha).toBeCloseTo(defWhiteAlpha, 2);
257
+
258
+ const defBlackAlpha = getAlpha(blackADefault[i]);
259
+ const genBlackAlpha = getAlpha(blackAGenerated[i]);
260
+ expect(genBlackAlpha).toBeCloseTo(defBlackAlpha, 2);
261
+ }
262
+ });
263
+ });
264
+
265
+ describe("white/black tinting across hues", () => {
266
+ it("white/black should be tinted for all hues when chroma > 0", () => {
267
+ const hues = Array.from({length: 36}, (_, i) => i * 10);
268
+
269
+ for (const hue of hues) {
270
+ const palette = makeThematicPalette({baseHue: hue, baseChroma: 0.175, accentHue: hue, accentChroma: 0.16});
271
+ // white should not be pure white
272
+ expect(palette.white).not.toBe("rgb(255,255,255)");
273
+ expect(palette.white).not.toBe("#FFFFFF");
274
+ // black should not be pure black
275
+ expect(palette.black).not.toBe("rgb(0,0,0)");
276
+ expect(palette.black).not.toBe("#000000");
277
+ }
278
+ });
279
+ });
280
+
281
+ describe("createPalette with default config", () => {
282
+ it("should closely match default palette lightness", () => {
283
+ const defaultPalette = makeDefaultPalette();
284
+ const generatedPalette = makeThematicPalette({
285
+ baseHue: 0,
286
+ baseChroma: 0,
287
+ accentHue: 264,
288
+ accentChroma: 0.16,
289
+ });
290
+
291
+ const defaultBase = extractLightness(defaultPalette.base);
292
+ const generatedBase = extractLightness(generatedPalette.base);
293
+ const defaultBaseDark = extractLightness(defaultPalette.baseDark);
294
+ const generatedBaseDark = extractLightness(generatedPalette.baseDark);
295
+ const defaultAccent = extractLightness(defaultPalette.accent);
296
+ const generatedAccent = extractLightness(generatedPalette.accent);
297
+ const defaultAccentDark = extractLightness(defaultPalette.accentDark);
298
+ const generatedAccentDark = extractLightness(generatedPalette.accentDark);
299
+
300
+ // Allow 0.5% lightness tolerance due to color space conversion
301
+ for (let i = 0; i < 12; i++) {
302
+ expect(Math.abs(generatedBase[i] - defaultBase[i])).toBeLessThan(0.5);
303
+ expect(Math.abs(generatedBaseDark[i] - defaultBaseDark[i])).toBeLessThan(0.5);
304
+ expect(Math.abs(generatedAccent[i] - defaultAccent[i])).toBeLessThan(0.5);
305
+ expect(Math.abs(generatedAccentDark[i] - defaultAccentDark[i])).toBeLessThan(0.5);
306
+ }
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,160 @@
1
+ import {slateUserPalette} from "./palettes/slate-user";
2
+ import {slateArchPalette} from "./palettes/slate-arch";
3
+ import {warmUserPalette} from "./palettes/warm-user";
4
+ import {warmArchPalette} from "./palettes/warm-arch";
5
+ import {
6
+ DEFAULT_DARK_CURVES,
7
+ DEFAULT_LIGHT_CURVES,
8
+ generateBlacks,
9
+ generateColorScale,
10
+ generateGrayScale,
11
+ generateInterpolatedScale,
12
+ generatePresetScale,
13
+ generateWhites,
14
+ type InterpolatedScaleConfig,
15
+ OneBasedScale,
16
+ paletteFromHue,
17
+ paletteFromHueBoth,
18
+ SCALE_PRESETS,
19
+ type ScaleConfig,
20
+ type ScaleCurves,
21
+ type ZeroBasedScale,
22
+ } from "./scale-generator";
23
+
24
+ export type ColorScale = OneBasedScale<string>;
25
+
26
+ export interface ThemePalette {
27
+ key: string;
28
+ white: string;
29
+ black: string;
30
+ whiteA: ColorScale;
31
+ blackA: ColorScale;
32
+ base: ColorScale;
33
+ baseDark: ColorScale;
34
+ accent: ColorScale;
35
+ accentDark: ColorScale;
36
+ red: ColorScale;
37
+ redDark: ColorScale;
38
+ teal: ColorScale;
39
+ tealDark: ColorScale;
40
+ yellow: ColorScale;
41
+ yellowDark: ColorScale;
42
+ }
43
+
44
+ export const defaultPaletteKey = slateUserPalette.key;
45
+
46
+ export function makeDefaultPalette(): ThemePalette {
47
+ return slateUserPalette;
48
+ }
49
+
50
+ export function selectPalette({
51
+ warmLook = false,
52
+ isArchitect = false,
53
+ }: {
54
+ warmLook?: boolean;
55
+ isArchitect?: boolean;
56
+ } = {}): ThemePalette {
57
+ if (warmLook) {
58
+ return isArchitect ? warmArchPalette : warmUserPalette;
59
+ } else {
60
+ return isArchitect ? slateArchPalette : slateUserPalette;
61
+ }
62
+ }
63
+
64
+ export type ThematicPrefs = {
65
+ baseHue?: number;
66
+ baseChroma?: number;
67
+ accentHue: number;
68
+ accentChroma: number;
69
+ contrast?: number;
70
+ whiteLuminosity?: number;
71
+ whiteChroma?: number;
72
+ blackLuminosity?: number;
73
+ blackChroma?: number;
74
+ };
75
+
76
+ function shiftHueTowards(hue: number, targetHue: number, amount: number): number {
77
+ // Find shortest path on hue circle
78
+ let diff = targetHue - hue;
79
+ if (diff > 180) {
80
+ diff -= 360;
81
+ } else if (diff < -180) {
82
+ diff += 360;
83
+ }
84
+ return (hue + diff * amount + 360) % 360;
85
+ }
86
+
87
+ export function makeThematicPalette(config: ThematicPrefs): ThemePalette {
88
+ const {baseHue = 0, baseChroma = 0, accentHue, accentChroma, contrast} = config;
89
+ const {white, whiteA} = generateWhites(baseHue, baseChroma, config.whiteLuminosity, config.whiteChroma);
90
+ const {black, blackA} = generateBlacks(baseHue, baseChroma, config.blackLuminosity, config.blackChroma);
91
+
92
+ // Generate base/gray scales with tinted base colors
93
+ const base = generateGrayScale({hue: baseHue, chroma: baseChroma, mode: "light", contrast});
94
+ const baseDark = generateGrayScale({hue: baseHue, chroma: baseChroma, mode: "dark", contrast});
95
+
96
+ // Generate accent scales
97
+ const accent = generateColorScale({hue: accentHue, chroma: accentChroma, mode: "light", contrast});
98
+ const accentDark = generateColorScale({hue: accentHue, chroma: accentChroma, mode: "dark", contrast});
99
+
100
+ // Shift semantic hues towards base hue when baseChroma is high
101
+ const hueShift = Math.min(baseChroma * 2, 0.3); // max 30% shift at chroma 0.15+
102
+ const redHue = shiftHueTowards(SCALE_PRESETS.red.hue, baseHue, hueShift);
103
+ const tealHue = shiftHueTowards(SCALE_PRESETS.teal.hue, baseHue, hueShift);
104
+ const yellowHue = shiftHueTowards(SCALE_PRESETS.yellow.hue, baseHue, hueShift);
105
+
106
+ const red = generateColorScale({hue: redHue, chroma: SCALE_PRESETS.red.chroma, mode: "light", contrast});
107
+ const redDark = generateColorScale({hue: redHue, chroma: SCALE_PRESETS.red.chroma, mode: "dark", contrast});
108
+ const teal = generateColorScale({hue: tealHue, chroma: SCALE_PRESETS.teal.chroma, mode: "light", contrast});
109
+ const tealDark = generateColorScale({hue: tealHue, chroma: SCALE_PRESETS.teal.chroma, mode: "dark", contrast});
110
+ const yellow = generateColorScale({
111
+ hue: yellowHue,
112
+ chroma: SCALE_PRESETS.yellow.chroma,
113
+ mode: "light",
114
+ contrast,
115
+ curves: SCALE_PRESETS.yellow.lightCurves,
116
+ });
117
+ const yellowDark = generateColorScale({
118
+ hue: yellowHue,
119
+ chroma: SCALE_PRESETS.yellow.chroma,
120
+ mode: "dark",
121
+ contrast,
122
+ curves: SCALE_PRESETS.yellow.darkCurves,
123
+ });
124
+
125
+ return {
126
+ key: JSON.stringify(config),
127
+ white,
128
+ black,
129
+ whiteA,
130
+ blackA,
131
+ base,
132
+ baseDark,
133
+ accent,
134
+ accentDark,
135
+ red,
136
+ redDark,
137
+ teal,
138
+ tealDark,
139
+ yellow,
140
+ yellowDark,
141
+ };
142
+ }
143
+
144
+ export {
145
+ generateBlacks,
146
+ generateColorScale,
147
+ generateGrayScale,
148
+ generateInterpolatedScale,
149
+ generatePresetScale,
150
+ generateWhites,
151
+ paletteFromHue,
152
+ paletteFromHueBoth,
153
+ SCALE_PRESETS,
154
+ DEFAULT_LIGHT_CURVES,
155
+ DEFAULT_DARK_CURVES,
156
+ type ZeroBasedScale,
157
+ type ScaleConfig,
158
+ type ScaleCurves,
159
+ type InterpolatedScaleConfig,
160
+ };
@@ -0,0 +1,72 @@
1
+ import {makeThemeDefs} from "../design-system/colors";
2
+ import {HSL, isValidColor, OKLCH, produceColor, sRGB} from "../color-utils";
3
+ import {ThemePalette} from "../palette-generator";
4
+
5
+ function verboseColor(color: string) {
6
+ return `${produceColor(color, [], sRGB)} / ${produceColor(color, [], OKLCH)} / ${produceColor(color, [], HSL)}`;
7
+ }
8
+
9
+ const COLOR_PREFIXES = ["rgba(", "rgb(", "hsla(", "hsl("] as const;
10
+ const HEX_CHARS = "0123456789abcdefABCDEF";
11
+
12
+ function replaceColorsInString(str: string) {
13
+ let result = "";
14
+ let i = 0;
15
+ const seen = new Set<string>();
16
+ while (i < str.length) {
17
+ const position = i;
18
+ const prefix = COLOR_PREFIXES.find((p) => str.startsWith(p, position));
19
+ if (prefix) {
20
+ const closeIdx = str.indexOf(")", i + prefix.length);
21
+ if (closeIdx !== -1) {
22
+ const candidate = str.slice(i, closeIdx + 1);
23
+ if (isValidColor(candidate)) {
24
+ const verbose = verboseColor(candidate);
25
+ result += seen.has(verbose) ? "[SAME]" : verbose;
26
+ seen.add(verbose);
27
+ i = closeIdx + 1;
28
+ continue;
29
+ }
30
+ }
31
+ }
32
+ if (str[i] === "#") {
33
+ let end = i + 1;
34
+ while (end < str.length && HEX_CHARS.includes(str[end])) {
35
+ end++;
36
+ }
37
+ const candidate = str.slice(i, end);
38
+ if (candidate.length >= 4 && isValidColor(candidate)) {
39
+ const verbose = verboseColor(candidate);
40
+ result += seen.has(verbose) ? "[SAME]" : verbose;
41
+ seen.add(verbose);
42
+ i = end;
43
+ continue;
44
+ }
45
+ }
46
+ result += str[i];
47
+ i++;
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function deepMapColors(value: unknown): unknown {
53
+ if (typeof value === "string") {
54
+ if (isValidColor(value)) {
55
+ return verboseColor(value);
56
+ }
57
+ return replaceColorsInString(value);
58
+ }
59
+ if (Array.isArray(value)) {
60
+ return value.map(deepMapColors);
61
+ }
62
+ if (typeof value === "object" && value !== null) {
63
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepMapColors(v)]));
64
+ }
65
+ return value;
66
+ }
67
+
68
+ export function colors(palette: ThemePalette) {
69
+ const {colors} = makeThemeDefs(palette);
70
+
71
+ return deepMapColors(colors);
72
+ }