@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,356 @@
1
+ import {fromOKLCH} from "./color-utils";
2
+ import type {ColorScale} from "./palette-generator";
3
+
4
+ export type OneBasedScale<T> = readonly [
5
+ base00: null,
6
+ step01: T,
7
+ step02: T,
8
+ step03: T,
9
+ step04: T,
10
+ step05: T,
11
+ step06: T,
12
+ step07: T,
13
+ step08: T,
14
+ step09: T,
15
+ step10: T,
16
+ step11: T,
17
+ step12: T
18
+ ];
19
+
20
+ export type ZeroBasedScale<T> = readonly [
21
+ step01: T,
22
+ step02: T,
23
+ step03: T,
24
+ step04: T,
25
+ step05: T,
26
+ step06: T,
27
+ step07: T,
28
+ step08: T,
29
+ step09: T,
30
+ step10: T,
31
+ step11: T,
32
+ step12: T
33
+ ];
34
+
35
+ export function zeroBasedFromOneBased<T>(scale: OneBasedScale<T>) {
36
+ return scale.slice(1) as unknown as ZeroBasedScale<T>;
37
+ }
38
+
39
+ export interface ScaleCurves {
40
+ lightness: ZeroBasedScale<number>; // 0-100 scale
41
+ chroma: ZeroBasedScale<number>; // multipliers relative to peakChroma
42
+ }
43
+
44
+ export interface ScaleConfig {
45
+ hue: number;
46
+ chroma: number;
47
+ mode: "light" | "dark";
48
+ curves?: ScaleCurves;
49
+ /** Adjusts lightness contrast. >1 = more contrast, <1 = less. Default 1. */
50
+ contrast?: number;
51
+ /** Override step 0 (base) color. Defaults to white. */
52
+ base?: string;
53
+ }
54
+
55
+ // Based on indigo scale (default accent) from palette.ts
56
+ export const DEFAULT_LIGHT_CURVES: ScaleCurves = {
57
+ lightness: [99.069, 98.983, 96.966, 95.012, 92.145, 88.367, 82.757, 76.14, 58.48, 54.173, 49.642, 26.071],
58
+ chroma: [0.018, 0.027, 0.08, 0.126, 0.19, 0.27, 0.384, 0.529, 0.934, 1.0, 0.878, 0.44],
59
+ };
60
+
61
+ export const DEFAULT_DARK_CURVES: ScaleCurves = {
62
+ lightness: [20.512, 22.974, 27.284, 29.742, 32.452, 35.981, 41.256, 47.972, 57.658, 62.928, 76.225, 95.899],
63
+ chroma: [0.119, 0.226, 0.357, 0.444, 0.475, 0.517, 0.72, 0.952, 1.0, 0.921, 0.601, 0.111],
64
+ };
65
+
66
+ // Curves extracted from current palette.ts
67
+ const SLATE_LIGHT_CURVES: ScaleCurves = {
68
+ lightness: [99.241, 97.719, 96.955, 94.656, 93.115, 91.568, 89.235, 82.937, 69.959, 57.217, 48.318, 20.441],
69
+ chroma: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
70
+ };
71
+
72
+ const SLATE_DARK_CURVES: ScaleCurves = {
73
+ lightness: [20.441, 22.667, 25.908, 28.012, 30.078, 32.109, 36.079, 43.714, 54.586, 59.818, 70.787, 94.656],
74
+ chroma: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
75
+ };
76
+
77
+ const INDIGO_LIGHT_CURVES: ScaleCurves = {
78
+ lightness: [99.069, 98.983, 96.966, 95.012, 92.145, 88.367, 82.757, 76.14, 58.48, 54.173, 49.642, 26.071],
79
+ chroma: [0.018, 0.027, 0.08, 0.126, 0.19, 0.27, 0.384, 0.529, 0.934, 1.0, 0.878, 0.44],
80
+ };
81
+
82
+ const INDIGO_DARK_CURVES: ScaleCurves = {
83
+ lightness: [20.512, 22.974, 27.284, 29.742, 32.452, 35.981, 41.256, 47.972, 57.658, 62.928, 76.225, 95.899],
84
+ chroma: [0.119, 0.226, 0.357, 0.444, 0.475, 0.517, 0.72, 0.952, 1.0, 0.921, 0.601, 0.111],
85
+ };
86
+
87
+ const RED_LIGHT_CURVES: ScaleCurves = {
88
+ lightness: [98.9, 98.9, 96.6, 94.4, 91.4, 87.5, 81.8, 74.5, 62.5, 59.7, 55.7, 24.9],
89
+ chroma: [0.027, 0.027, 0.084, 0.138, 0.207, 0.289, 0.403, 0.562, 0.975, 0.985, 1.0, 0.307],
90
+ };
91
+
92
+ const RED_DARK_CURVES: ScaleCurves = {
93
+ lightness: [20.6, 22.2, 25.9, 28.7, 31.5, 35.2, 40.8, 48.1, 62.5, 66.2, 70.5, 95.7],
94
+ chroma: [0.101, 0.178, 0.288, 0.369, 0.441, 0.54, 0.687, 0.872, 1.0, 0.992, 0.967, 0.104],
95
+ };
96
+
97
+ const TEAL_LIGHT_CURVES: ScaleCurves = {
98
+ lightness: [99.4, 98.4, 96.7, 94.3, 91.2, 86.8, 80.5, 71.8, 65, 61.7, 51.8, 29],
99
+ chroma: [0.03, 0.094, 0.174, 0.255, 0.337, 0.449, 0.6, 0.859, 1.0, 0.953, 0.799, 0.353],
100
+ };
101
+
102
+ const TEAL_DARK_CURVES: ScaleCurves = {
103
+ lightness: [18.9, 22, 25.2, 28.4, 31.6, 34.7, 40.7, 48.5, 65, 68.8, 74.4, 96.5],
104
+ chroma: [0.163, 0.269, 0.31, 0.354, 0.4, 0.44, 0.528, 0.636, 0.864, 0.913, 1.0, 0.215],
105
+ };
106
+
107
+ const YELLOW_LIGHT_CURVES: ScaleCurves = {
108
+ lightness: [99.15, 98.666, 98.103, 97.119, 95.34, 92.061, 86.841, 81.273, 88.224, 85.516, 54.771, 28.162],
109
+ chroma: [0.04, 0.16, 0.299, 0.419, 0.539, 0.621, 0.692, 0.911, 1.0, 0.962, 0.624, 0.231],
110
+ };
111
+
112
+ const YELLOW_DARK_CURVES: ScaleCurves = {
113
+ lightness: [18.975, 22.715, 25.994, 27.672, 31.467, 35.323, 40.577, 48.421, 88.224, 93.814, 82.593, 97.801],
114
+ chroma: [0.213, 0.255, 0.291, 0.31, 0.353, 0.398, 0.444, 0.546, 1.0, 0.898, 0.926, 0.289],
115
+ };
116
+
117
+ // ============ PRESET CONFIGS ============
118
+ export const SCALE_PRESETS = {
119
+ slate: {
120
+ hue: 0,
121
+ chroma: 0,
122
+ lightCurves: SLATE_LIGHT_CURVES,
123
+ darkCurves: SLATE_DARK_CURVES,
124
+ },
125
+ indigo: {
126
+ hue: 264,
127
+ chroma: 0.1618, // peak chroma at step 10 light
128
+ lightCurves: INDIGO_LIGHT_CURVES,
129
+ darkCurves: INDIGO_DARK_CURVES,
130
+ },
131
+ red: {
132
+ hue: 19,
133
+ chroma: 0.1978, // peak chroma at step 11 light
134
+ lightCurves: RED_LIGHT_CURVES,
135
+ darkCurves: RED_DARK_CURVES,
136
+ },
137
+ teal: {
138
+ hue: 183,
139
+ chroma: 0.1137, // peak chroma at step 9 light
140
+ lightCurves: TEAL_LIGHT_CURVES,
141
+ darkCurves: TEAL_DARK_CURVES,
142
+ },
143
+ yellow: {
144
+ hue: 100, // OKLCH yellow hue
145
+ chroma: 0.19, // peak chroma at step 9 light
146
+ lightCurves: YELLOW_LIGHT_CURVES,
147
+ darkCurves: YELLOW_DARK_CURVES,
148
+ },
149
+ } as const;
150
+
151
+ /**
152
+ * Generate a 12-step color scale from hue + peak chroma.
153
+ * Returns ColorScale (13 elements: base + 12 steps)
154
+ */
155
+ function applyContrast(L: number, contrast: number): number {
156
+ // Scale lightness away from midpoint (50)
157
+ const adjusted = 50 + (L - 50) * contrast;
158
+ return Math.max(0, Math.min(100, adjusted));
159
+ }
160
+
161
+ export function generateColorScale(config: ScaleConfig): ColorScale {
162
+ const {hue, chroma, mode, curves, contrast = 1} = config;
163
+
164
+ const defaultCurves = mode === "light" ? DEFAULT_LIGHT_CURVES : DEFAULT_DARK_CURVES;
165
+ const {lightness, chroma: chromaCurve} = curves ?? defaultCurves;
166
+
167
+ const steps = lightness.map((L, i) => {
168
+ const adjustedL = applyContrast(L, contrast);
169
+ const C = chroma * chromaCurve[i];
170
+ return fromOKLCH(adjustedL / 100, C, hue);
171
+ });
172
+
173
+ return [null, ...steps] as unknown as ColorScale;
174
+ }
175
+
176
+ /**
177
+ * Generate a scale using a named preset
178
+ */
179
+ export function generatePresetScale(name: keyof typeof SCALE_PRESETS, mode: "light" | "dark"): ColorScale {
180
+ const preset = SCALE_PRESETS[name];
181
+ const curves = "lightCurves" in preset ? (mode === "light" ? preset.lightCurves : preset.darkCurves) : undefined;
182
+ return generateColorScale({
183
+ hue: preset.hue,
184
+ chroma: preset.chroma,
185
+ mode,
186
+ curves,
187
+ });
188
+ }
189
+
190
+ const GRAY_CURVES: ScaleCurves = {
191
+ // Based on slate (default base) from palette.ts
192
+ lightness: SLATE_LIGHT_CURVES.lightness,
193
+ chroma: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
194
+ };
195
+
196
+ const GRAY_DARK_CURVES: ScaleCurves = {
197
+ lightness: SLATE_DARK_CURVES.lightness,
198
+ chroma: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
199
+ };
200
+
201
+ /**
202
+ * Generate a gray scale with optional subtle hue tint
203
+ */
204
+ export function generateGrayScale(config: {
205
+ hue?: number;
206
+ chroma?: number;
207
+ mode: "light" | "dark";
208
+ contrast?: number;
209
+ }): ColorScale {
210
+ const {hue = 0, chroma = 0, mode, contrast} = config;
211
+ const curves = mode === "light" ? GRAY_CURVES : GRAY_DARK_CURVES;
212
+ return generateColorScale({hue, chroma, mode, contrast, curves});
213
+ }
214
+
215
+ // ============ HUE INTERPOLATION ============
216
+
217
+ interface HueReference {
218
+ hue: number;
219
+ chroma: number;
220
+ lightCurves: ScaleCurves;
221
+ darkCurves: ScaleCurves;
222
+ }
223
+
224
+ // Reference scales sorted by hue for interpolation
225
+ const HUE_REFERENCES: HueReference[] = [
226
+ {hue: 19, chroma: 0.1978, lightCurves: RED_LIGHT_CURVES, darkCurves: RED_DARK_CURVES},
227
+ {hue: 183, chroma: 0.1137, lightCurves: TEAL_LIGHT_CURVES, darkCurves: TEAL_DARK_CURVES},
228
+ {hue: 264, chroma: 0.1618, lightCurves: INDIGO_LIGHT_CURVES, darkCurves: INDIGO_DARK_CURVES},
229
+ ].sort((a, b) => a.hue - b.hue);
230
+
231
+ function lerp(a: number, b: number, t: number): number {
232
+ return a + (b - a) * t;
233
+ }
234
+
235
+ function lerpArray<T extends readonly number[]>(a: T, b: T, t: number): {[K in keyof T]: number} {
236
+ return a.map((v, i) => lerp(v, b[i], t)) as {[K in keyof T]: number};
237
+ }
238
+
239
+ function normalizeHue(h: number): number {
240
+ return ((h % 360) + 360) % 360;
241
+ }
242
+
243
+ /**
244
+ * Find bracketing references and interpolation factor for a target hue
245
+ */
246
+ function findHueBracket(targetHue: number): {before: HueReference; after: HueReference; t: number} {
247
+ const h = normalizeHue(targetHue);
248
+ const refs = HUE_REFERENCES;
249
+
250
+ // Find which two references bracket the target hue
251
+ let beforeIdx = refs.length - 1;
252
+ let afterIdx = 0;
253
+
254
+ for (let i = 0; i < refs.length; i++) {
255
+ if (refs[i].hue <= h) {
256
+ beforeIdx = i;
257
+ afterIdx = (i + 1) % refs.length;
258
+ }
259
+ }
260
+
261
+ const before = refs[beforeIdx];
262
+ const after = refs[afterIdx];
263
+
264
+ // Calculate interpolation factor
265
+ const beforeHue = before.hue;
266
+ let afterHue = after.hue;
267
+
268
+ // Handle wrap-around (e.g., from indigo 264 to red 19)
269
+ if (afterHue <= beforeHue) {
270
+ afterHue += 360;
271
+ }
272
+
273
+ let targetH = h;
274
+ if (targetH < beforeHue) {
275
+ targetH += 360;
276
+ }
277
+
278
+ const range = afterHue - beforeHue;
279
+ const t = range > 0 ? (targetH - beforeHue) / range : 0;
280
+
281
+ return {before, after, t: Math.max(0, Math.min(1, t))};
282
+ }
283
+
284
+ /**
285
+ * Interpolate curves between two references
286
+ */
287
+ function interpolateCurves(before: ScaleCurves, after: ScaleCurves, t: number): ScaleCurves {
288
+ return {
289
+ lightness: lerpArray(before.lightness, after.lightness, t),
290
+ chroma: lerpArray(before.chroma, after.chroma, t),
291
+ };
292
+ }
293
+
294
+ export interface InterpolatedScaleConfig {
295
+ /** Target hue (0-360). Curves are interpolated from nearby reference scales. */
296
+ hue: number;
297
+ /** Optional chroma override. If not provided, interpolated from references. */
298
+ chroma?: number;
299
+ /** Light or dark mode */
300
+ mode: "light" | "dark";
301
+ }
302
+
303
+ /**
304
+ * Generate a scale for any hue by interpolating curves from reference scales.
305
+ * This produces smooth, natural-looking palettes for any color.
306
+ */
307
+ export function generateInterpolatedScale(config: InterpolatedScaleConfig): ColorScale {
308
+ const {hue, chroma: chromaOverride, mode} = config;
309
+
310
+ const {before, after, t} = findHueBracket(hue);
311
+
312
+ // Interpolate curves
313
+ const beforeCurves = mode === "light" ? before.lightCurves : before.darkCurves;
314
+ const afterCurves = mode === "light" ? after.lightCurves : after.darkCurves;
315
+ const curves = interpolateCurves(beforeCurves, afterCurves, t);
316
+
317
+ // Interpolate chroma if not overridden
318
+ const chroma = chromaOverride ?? lerp(before.chroma, after.chroma, t);
319
+
320
+ return generateColorScale({hue, chroma, mode, curves});
321
+ }
322
+
323
+ export function paletteFromHue(hue: number, mode: "light" | "dark"): ColorScale {
324
+ return generateInterpolatedScale({hue, mode});
325
+ }
326
+
327
+ export function paletteFromHueBoth(hue: number): {light: ColorScale; dark: ColorScale} {
328
+ return {
329
+ light: generateInterpolatedScale({hue, mode: "light"}),
330
+ dark: generateInterpolatedScale({hue, mode: "dark"}),
331
+ };
332
+ }
333
+
334
+ export function generateWhites(hue: number, chroma: number, luminosity = 1, chromaMul = 1) {
335
+ const c = chroma * 0.1 * chromaMul;
336
+ const l = (1 - chroma * 0.1) * luminosity;
337
+ return {
338
+ white: fromOKLCH(l, c, hue, 1),
339
+ whiteA: [
340
+ null,
341
+ ...[0, 0.01, 0.03, 0.06, 0.09, 0.12, 0.18, 0.25, 0.39, 0.45, 0.59, 0.92].map((a) => fromOKLCH(l, c, hue, a)),
342
+ ] as unknown as ColorScale,
343
+ };
344
+ }
345
+
346
+ export function generateBlacks(hue: number, chroma: number, luminosity = 1, chromaMul = 1) {
347
+ const c = chroma * 0.1 * chromaMul;
348
+ const l = chroma * 0.1 * 4 * luminosity;
349
+ return {
350
+ black: fromOKLCH(l, c, hue, 1),
351
+ blackA: [
352
+ null,
353
+ ...[0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.14, 0.22, 0.44, 0.48, 0.57, 0.91].map((a) => fromOKLCH(l, c, hue, a)),
354
+ ] as unknown as ColorScale,
355
+ };
356
+ }
@@ -1,5 +1,6 @@
1
- import {css} from "@linaria/core";
2
- import _, {isFunction, isObject} from "lodash";
1
+ import compact from "lodash/compact";
2
+ import isFunction from "lodash/isFunction";
3
+ import isObject from "lodash/isObject";
3
4
  import {
4
5
  Children,
5
6
  ComponentType,
@@ -18,23 +19,9 @@ import {GroupBase, ReactSelectRefContext} from "../util";
18
19
  import {useSelectControlSettings} from "../select-control-settings-context";
19
20
  import {useIsPhone} from "../../use-is-phone";
20
21
 
21
- const PADDING_BLOCK_HEIGHT = 6;
22
-
23
- const Spacer = () => (
24
- <div
25
- className={css`
26
- height: ${PADDING_BLOCK_HEIGHT}px;
27
- `}
28
- />
29
- );
30
-
31
- const components = {
32
- Footer: Spacer,
33
- Header: Spacer,
34
- };
35
-
36
22
  // Threshold to determine when to scroll: if focused item is less than this visible, scroll it into view
37
23
  const MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW = 0.2;
24
+
38
25
  function isGroupChild<Option, isMulty extends boolean, Group extends GroupBase<Option> = GroupBase<Option>>(
39
26
  props: unknown
40
27
  ): props is GroupProps<Option, isMulty, Group> {
@@ -47,18 +34,19 @@ function isGroupChild<Option, isMulty extends boolean, Group extends GroupBase<O
47
34
  isObject(props.headingProps)
48
35
  );
49
36
  }
37
+
50
38
  const flattenGroupChildren: (
51
39
  children: ReactNode,
52
40
  renderGroup: (Component: ComponentType<GroupHeadingProps>, props: GroupProps["headingProps"]) => ReactNode
53
41
  ) => ReactNode[] = (children, renderGroup) => {
54
- return _.compact(
42
+ return compact(
55
43
  Children.toArray(children)
56
44
  .map((child) => {
57
45
  if (isObject(child) && "props" in child && isObject(child.props)) {
58
46
  const {props} = child;
59
47
  if (isGroupChild(props)) {
60
48
  //well, it's a group
61
- const children = _.compact(Children.toArray(props.children));
49
+ const children = compact(Children.toArray(props.children));
62
50
  if (!children.length) {
63
51
  // do not render empty groups
64
52
  return [];
@@ -73,7 +61,9 @@ const flattenGroupChildren: (
73
61
  .flat()
74
62
  );
75
63
  };
64
+
76
65
  const drawListElement: ItemContent<ReactNode, never> = (index, child) => child;
66
+
77
67
  export function MenuListVirtualized<
78
68
  Option,
79
69
  IsMulti extends boolean = boolean,
@@ -117,7 +107,7 @@ export function MenuListVirtualized<
117
107
 
118
108
  // Calculate menu height
119
109
  const menuHeight = useMemo(
120
- () => Math.min(flattenChildren.length * itemHeight + PADDING_BLOCK_HEIGHT * 2, maxHeight),
110
+ () => Math.min(flattenChildren.length * itemHeight, maxHeight),
121
111
  [flattenChildren.length, itemHeight, maxHeight]
122
112
  );
123
113
 
@@ -188,10 +178,10 @@ export function MenuListVirtualized<
188
178
  }
189
179
 
190
180
  virtuoso.current.getState((state) => {
191
- const focusedItemTop = focusedIndex * itemHeight + PADDING_BLOCK_HEIGHT;
181
+ const focusedItemTop = focusedIndex * itemHeight;
192
182
  const focusedItemBottom = focusedItemTop + itemHeight;
193
183
  const viewportTop = state.scrollTop;
194
- const viewportBottom = state.scrollTop + menuHeight - PADDING_BLOCK_HEIGHT;
184
+ const viewportBottom = state.scrollTop + menuHeight;
195
185
 
196
186
  // Check if item is cut off at the top (even partially)
197
187
  const isTopCutOff = focusedItemTop < viewportTop;
@@ -232,18 +222,15 @@ export function MenuListVirtualized<
232
222
  }, [focusedOption, focusedIndex, initialFocusedOption, itemHeight, menuHeight, scrollThreshold]);
233
223
 
234
224
  return (
235
- // Needed for correct display inside portals and etc.
236
225
  <div style={getStyles("menuList", props) as CSSProperties}>
237
226
  <Virtuoso
238
227
  ref={virtuoso}
239
- // in reality only HTMLDivElement | null will appear here. We do not use Virtuoso's useWindowScroll prop
240
228
  scrollerRef={innerRef as (ref: HTMLElement | Window | null) => void}
241
229
  style={isPhone && inPopover ? undefined : {height: menuHeight}}
242
230
  fixedItemHeight={itemHeight}
243
231
  data={flattenChildren}
244
232
  totalCount={flattenChildren.length}
245
233
  itemContent={drawListElement}
246
- components={components}
247
234
  />
248
235
  </div>
249
236
  );
@@ -1,6 +1,8 @@
1
1
  import {css, cx} from "@linaria/core";
2
- import {CSSObjectWithLabel, MenuProps, components} from "react-select";
2
+ import {components, CSSObjectWithLabel, MenuProps} from "react-select";
3
+ import type {MenuPortalProps} from "react-select/dist/declarations/src/components/Menu";
3
4
  import {border, space, themeVars} from "../../design-system";
5
+ import {ThemeProvider} from "../../theme-provider";
4
6
  import {GroupBase} from "../index";
5
7
 
6
8
  export const MenuListNulledStyles: CSSObjectWithLabel = {
@@ -20,7 +22,7 @@ export const MenuNulledStyles: CSSObjectWithLabel = {
20
22
  const menuCss = css`
21
23
  color: ${themeVars.disabledTextColor};
22
24
  z-index: 1050;
23
- background-color: ${themeVars.colorBgSelectMenu};
25
+ background-color: ${themeVars.colorBgPopup};
24
26
  box-shadow: ${themeVars.shadowSelectMenu};
25
27
  border-radius: ${border.radius6}px;
26
28
  overflow: hidden;
@@ -38,3 +40,11 @@ const menuTransparentCss = css`
38
40
  export function MenuTransparent<T, U extends boolean, V extends GroupBase<T>>(props: MenuProps<T, U, V>) {
39
41
  return <components.Menu {...props} className={cx(menuTransparentCss, "react-select-menu", props.className)} />;
40
42
  }
43
+
44
+ export function MenuPortal<T, U extends boolean, V extends GroupBase<T>>(props: MenuPortalProps<T, U, V>) {
45
+ return (
46
+ <components.MenuPortal {...props}>
47
+ <ThemeProvider portal>{props.children}</ThemeProvider>
48
+ </components.MenuPortal>
49
+ );
50
+ }
@@ -1,10 +1,8 @@
1
- /* eslint-disable no-use-before-define */
2
1
  import {useIsPhone} from "../use-is-phone";
3
- import {SelectComponent} from "./select";
2
+ import type {GroupBase, SelectInstance, SelectProps} from "./select";
3
+ import {combineStyles, SelectComponent} from "./select";
4
4
  import React, {ForwardedRef, forwardRef} from "react";
5
- import type {SelectProps, GroupBase, SelectInstance} from "./select";
6
5
  import {SelectInPopover} from "./select-in-popover";
7
- import {combineStyles} from "./select";
8
6
  import {makeSingleLineStyles} from "./styles";
9
7
  import {css} from "@linaria/core";
10
8
  import {$TSFixMe} from "../tsfixme";
@@ -52,12 +50,13 @@ export const Select = forwardRef(function SelectInner<
52
50
  placeholder,
53
51
  title,
54
52
  isSearchable,
53
+ inline,
55
54
  ...restProps
56
55
  }: SelectProps<T, U, V>,
57
56
  forwardedRef: ForwardedRef<SelectInstance<T, U, V>>
58
57
  ) {
59
58
  const isPhone = useIsPhone();
60
- if (isPhone) {
59
+ if (isPhone && !inline) {
61
60
  return (
62
61
  <SelectInPopover
63
62
  {...(restProps as $TSFixMe)}
@@ -106,6 +105,7 @@ export const Select = forwardRef(function SelectInner<
106
105
  </div>
107
106
  );
108
107
  }}
108
+ inline={inline}
109
109
  />
110
110
  );
111
111
  }
@@ -119,6 +119,7 @@ export const Select = forwardRef(function SelectInner<
119
119
  isOptionDisabled={isOptionDisabled}
120
120
  isClearable={isClearable}
121
121
  styles={styles}
122
+ inline={inline}
122
123
  {...restProps}
123
124
  />
124
125
  );
@@ -1,13 +1,12 @@
1
- /* eslint-disable no-use-before-define */
2
1
  import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
3
2
  import {ForwardedRef, forwardRef, RefObject, useCallback, useMemo, useRef, useState} from "react";
4
3
  import BaseSelect, {
5
4
  ActionMeta,
6
5
  components as reactSelectComponents,
6
+ ControlProps,
7
7
  createFilter,
8
8
  MenuListProps,
9
9
  MenuProps,
10
- ControlProps,
11
10
  mergeStyles,
12
11
  MultiValue,
13
12
  MultiValueProps,
@@ -25,14 +24,15 @@ import BaseCreatableSelect, {CreatableProps} from "react-select/creatable";
25
24
  import {ClearIndicator, NoClearIndicator} from "./components/clear-indicator";
26
25
  import {DropdownIndicator} from "./components/drop-down-indicator";
27
26
  import {GroupHeading} from "./components/group-heading";
28
- import {Menu} from "./components/menu";
27
+ import {Menu, MenuPortal} from "./components/menu";
29
28
  import {MenuListVirtualized} from "./components/menu-list-virtualized";
30
29
  import {NoOptionsMessage} from "./components/no-option-message";
31
30
  import {Option, OptionSlow} from "./components/option";
32
31
  import {useSelectControlSettings} from "./select-control-settings-context";
33
32
  import {makeComponentsStyles, makeNonVirtualizedStyles, makeVirtualizedStyles, makeZIndexStyles} from "./styles";
34
33
  import {countOptions, GroupBase, ReactSelectRefContext} from "./util";
35
- import _ from "lodash";
34
+ import compact from "lodash/compact";
35
+ import {composeEventHandlers} from "@fibery/react/src/compose-event-handlers";
36
36
 
37
37
  export type {
38
38
  ActionMeta,
@@ -72,6 +72,7 @@ export type SelectProps<T = unknown, U extends boolean = boolean, V extends Grou
72
72
  isCollectionMode?: U;
73
73
  disabled?: boolean;
74
74
  forbidValuesClearInCollnMode?: boolean;
75
+ inline?: boolean;
75
76
  };
76
77
 
77
78
  const FAST_OPTION_USAGE_THRESHOLD = 200;
@@ -94,6 +95,7 @@ function useSelectComponents<T, U extends boolean, V extends GroupBase<T>>({
94
95
 
95
96
  return {
96
97
  Menu,
98
+ MenuPortal,
97
99
  DropdownIndicator,
98
100
  ClearIndicator,
99
101
  MultiValueRemove: shouldHideMultiValueRemove ? NoClearIndicator : ClearIndicator,
@@ -121,7 +123,7 @@ function useSelectStyles<T, U extends boolean, V extends GroupBase<T>>({
121
123
  }) {
122
124
  return useMemo(() => {
123
125
  return combineStyles<T, U, V>(
124
- _.compact([
126
+ compact([
125
127
  makeComponentsStyles<T, U, V>(),
126
128
  virtualized ? makeVirtualizedStyles<T, U, V>() : makeNonVirtualizedStyles<T, U, V>(),
127
129
  zIndex ? makeZIndexStyles<T, U, V>(zIndex) : null,
@@ -140,13 +142,14 @@ export const SelectComponent = forwardRef(function Select<
140
142
  components,
141
143
  isCollectionMode,
142
144
  menuPortalTarget,
143
- onKeyDown,
144
145
  styles,
145
146
  virtualized = true,
147
+ forbidValuesClearInCollnMode,
148
+ inline,
146
149
  onMenuOpen,
147
150
  onMenuClose,
148
151
  onChange,
149
- forbidValuesClearInCollnMode,
152
+ onKeyDown,
150
153
  ...rest
151
154
  }: SelectProps<T, U, V>,
152
155
  forwardedRef: ForwardedRef<SelectInstance<T, U, V>>
@@ -165,39 +168,35 @@ export const SelectComponent = forwardRef(function Select<
165
168
  onMenuClose && onMenuClose();
166
169
  }, [onMenuClose]);
167
170
 
168
- const handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = useCallback(
169
- (e) => {
170
- switch (e.key) {
171
- case "Escape":
172
- // we can't rely on "menuIsOpen" prop here, as user may not pass this prop and rely on internal component behavior instead.
173
- if (menuOpenState) {
174
- e.stopPropagation();
175
- }
176
- break;
177
- case "Home":
178
- e.preventDefault();
179
- if (e.shiftKey) {
180
- (e.target as HTMLInputElement).selectionStart = 0;
181
- } else {
182
- (e.target as HTMLInputElement).setSelectionRange(0, 0);
183
- }
184
- break;
185
- case "End": {
186
- e.preventDefault();
187
- const len = (e.target as HTMLInputElement).value.length;
188
- if (e.shiftKey) {
189
- (e.target as HTMLInputElement).selectionEnd = len;
190
- } else {
191
- (e.target as HTMLInputElement).setSelectionRange(len, len);
192
- }
193
- break;
171
+ const onKeyDownComposed = composeEventHandlers(onKeyDown, (e) => {
172
+ switch (e.key) {
173
+ case "Escape":
174
+ // we can't rely on "menuIsOpen" prop here, as user may not pass this prop and rely on internal component behavior instead.
175
+ if (menuOpenState && !inline) {
176
+ e.stopPropagation();
177
+ }
178
+ break;
179
+ case "Home":
180
+ e.preventDefault();
181
+ if (e.shiftKey) {
182
+ (e.target as HTMLInputElement).selectionStart = 0;
183
+ } else {
184
+ (e.target as HTMLInputElement).setSelectionRange(0, 0);
185
+ }
186
+ break;
187
+ case "End": {
188
+ e.preventDefault();
189
+ const len = (e.target as HTMLInputElement).value.length;
190
+ if (e.shiftKey) {
191
+ (e.target as HTMLInputElement).selectionEnd = len;
192
+ } else {
193
+ (e.target as HTMLInputElement).setSelectionRange(len, len);
194
194
  }
195
- default:
195
+ break;
196
196
  }
197
- onKeyDown && onKeyDown(e);
198
- },
199
- [onKeyDown, menuOpenState]
200
- );
197
+ default:
198
+ }
199
+ });
201
200
 
202
201
  const selectRef = useRef<SelectInstance<T, U, V> | null>(null);
203
202
  const composedRef = useComposedRefs(forwardedRef, selectRef);
@@ -226,7 +225,7 @@ export const SelectComponent = forwardRef(function Select<
226
225
  tabSelectsValue={false}
227
226
  components={selectComponents}
228
227
  {...rest}
229
- onKeyDown={handleKeyDown}
228
+ onKeyDown={onKeyDownComposed}
230
229
  onMenuOpen={onMenuOpenWrapped}
231
230
  onMenuClose={onMenuCloseWrapped}
232
231
  onChange={rest.disabled ? undefined : onChange}
@@ -190,7 +190,6 @@ export const makeSelectInPopoverStyles = <T, U extends boolean, V extends GroupB
190
190
  margin: space.s2,
191
191
  marginBottom: space.s4,
192
192
  width: `calc(100% - ${space.s2 * 2}px)`,
193
- borderRadius: border.radius4,
194
193
  }),
195
194
  menuList: (props) => {
196
195
  if (isPhone) {