@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,411 @@
1
+ import {act, render, screen} from "@testing-library/react";
2
+ import {
3
+ RootThemeProvider,
4
+ ThemeProvider,
5
+ useThematic,
6
+ useTheme,
7
+ useThemeMenuPreference,
8
+ useThemeMode,
9
+ useThemePreference,
10
+ } from "./theme-provider";
11
+ import type {ThemePalette} from "./palette-generator";
12
+ import {defaultDarkTheme, defaultLightTheme, defaultPalette} from "./theme-styles";
13
+ import {createInlineStyles, getThemeColors} from "./design-system";
14
+ import {storeWarmPreference} from "./theme-settings";
15
+
16
+ function ThemeDisplay({testId = "theme-display"}: {testId?: string}) {
17
+ const theme = useTheme();
18
+ const mode = useThemeMode();
19
+ return (
20
+ <div data-testid={testId} data-mode={mode} data-primary={theme.primary}>
21
+ {theme.key}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ function PreferenceDisplay() {
27
+ const preference = useThemePreference();
28
+ const menuPreference = useThemeMenuPreference();
29
+ return (
30
+ <div data-testid="preferences" data-preference={preference} data-menu-preference={menuPreference}>
31
+ {preference}/{menuPreference}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function ThematicDisplay() {
37
+ const {palette, setPalette} = useThematic();
38
+ return (
39
+ <div data-testid="thematic" data-has-palette={Boolean(palette)} data-has-setter={typeof setPalette === "function"}>
40
+ thematic
41
+ </div>
42
+ );
43
+ }
44
+
45
+ describe("RootThemeProvider", () => {
46
+ beforeEach(() => {
47
+ document.documentElement.className = "";
48
+ document.documentElement.style.colorScheme = "";
49
+ localStorage.clear();
50
+ });
51
+
52
+ describe("context provision", () => {
53
+ it("provides theme via useTheme", () => {
54
+ render(
55
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
56
+ <ThemeDisplay />
57
+ </RootThemeProvider>
58
+ );
59
+ expect(screen.getByTestId("theme-display")).toHaveAttribute("data-mode", "light");
60
+ });
61
+
62
+ it("provides dark theme when initialThemeMode is dark", () => {
63
+ render(
64
+ <RootThemeProvider initialThemeMode="dark" initialPreference="dark" initialMenuPreference="dark">
65
+ <ThemeDisplay />
66
+ </RootThemeProvider>
67
+ );
68
+ expect(screen.getByTestId("theme-display")).toHaveAttribute("data-mode", "dark");
69
+ });
70
+
71
+ it("provides themePreference via useThemePreference", () => {
72
+ render(
73
+ <RootThemeProvider initialThemeMode="light" initialPreference="auto" initialMenuPreference="auto">
74
+ <PreferenceDisplay />
75
+ </RootThemeProvider>
76
+ );
77
+ expect(screen.getByTestId("preferences")).toHaveAttribute("data-preference", "auto");
78
+ });
79
+
80
+ it("provides themeMenuPreference via useThemeMenuPreference", () => {
81
+ render(
82
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="dark">
83
+ <PreferenceDisplay />
84
+ </RootThemeProvider>
85
+ );
86
+ expect(screen.getByTestId("preferences")).toHaveAttribute("data-menu-preference", "dark");
87
+ });
88
+
89
+ it("provides thematic context with palette and setPalette", () => {
90
+ render(
91
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
92
+ <ThematicDisplay />
93
+ </RootThemeProvider>
94
+ );
95
+ const el = screen.getByTestId("thematic");
96
+ expect(el).toHaveAttribute("data-has-palette", "true");
97
+ expect(el).toHaveAttribute("data-has-setter", "true");
98
+ });
99
+ });
100
+
101
+ describe("base context for diff optimization", () => {
102
+ it("sets light base context in light mode", () => {
103
+ const {container} = render(
104
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
105
+ <ThemeProvider theme={defaultLightTheme}>
106
+ <span data-testid="inner">inner</span>
107
+ </ThemeProvider>
108
+ </RootThemeProvider>
109
+ );
110
+
111
+ // Inner ThemeProvider with same light theme should have empty diff
112
+ const divs = Array.from(container.querySelectorAll("div")).filter((d) => d.hasAttribute("style"));
113
+ // The innermost div (from nested ThemeProvider) should have no styles
114
+ const innerMostDiv = divs[divs.length - 1];
115
+ const style = innerMostDiv?.getAttribute("style") || "";
116
+ expect(style).toBe("");
117
+ });
118
+
119
+ it("sets dark base context in dark mode", () => {
120
+ const {container} = render(
121
+ <RootThemeProvider initialThemeMode="dark" initialPreference="dark" initialMenuPreference="dark">
122
+ <ThemeProvider theme={defaultDarkTheme}>
123
+ <span data-testid="inner">inner</span>
124
+ </ThemeProvider>
125
+ </RootThemeProvider>
126
+ );
127
+
128
+ // Inner ThemeProvider with same dark theme should have empty diff
129
+ const divs = Array.from(container.querySelectorAll("div")).filter((d) => d.hasAttribute("style"));
130
+ const innerMostDiv = divs[divs.length - 1];
131
+ const style = innerMostDiv?.getAttribute("style") || "";
132
+ expect(style).toBe("");
133
+ });
134
+
135
+ it("nested light ThemeProvider in dark RootThemeProvider produces diff", () => {
136
+ const {container} = render(
137
+ <RootThemeProvider initialThemeMode="dark" initialPreference="dark" initialMenuPreference="dark">
138
+ <ThemeProvider theme={defaultLightTheme}>
139
+ <span data-testid="inner">inner</span>
140
+ </ThemeProvider>
141
+ </RootThemeProvider>
142
+ );
143
+
144
+ // Inner ThemeProvider with light theme should have diff (dark->light)
145
+ const divs = Array.from(container.querySelectorAll("div")).filter((d) => d.hasAttribute("style"));
146
+ const innerMostDiv = divs[divs.length - 1];
147
+ const style = innerMostDiv?.getAttribute("style") || "";
148
+ expect(style.length).toBeGreaterThan(0);
149
+ });
150
+ });
151
+
152
+ describe("document element classes", () => {
153
+ it("adds dark-theme class in dark mode", () => {
154
+ render(
155
+ <RootThemeProvider initialThemeMode="dark" initialPreference="dark" initialMenuPreference="dark">
156
+ <div>content</div>
157
+ </RootThemeProvider>
158
+ );
159
+ expect(document.documentElement.classList.contains("dark-theme")).toBe(true);
160
+ });
161
+
162
+ it("does not add dark-theme class in light mode", () => {
163
+ render(
164
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
165
+ <div>content</div>
166
+ </RootThemeProvider>
167
+ );
168
+ expect(document.documentElement.classList.contains("dark-theme")).toBe(false);
169
+ });
170
+
171
+ it("adds light-theme-and-dark-menu class in light2 mode", () => {
172
+ render(
173
+ <RootThemeProvider initialThemeMode="light2" initialPreference="light" initialMenuPreference="dark">
174
+ <div>content</div>
175
+ </RootThemeProvider>
176
+ );
177
+ expect(document.documentElement.classList.contains("light-theme-and-dark-menu")).toBe(true);
178
+ });
179
+
180
+ it("sets colorScheme to dark in dark mode", () => {
181
+ render(
182
+ <RootThemeProvider initialThemeMode="dark" initialPreference="dark" initialMenuPreference="dark">
183
+ <div>content</div>
184
+ </RootThemeProvider>
185
+ );
186
+ expect(document.documentElement.style.colorScheme).toBe("dark");
187
+ });
188
+
189
+ it("sets colorScheme to light in light mode", () => {
190
+ render(
191
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
192
+ <div>content</div>
193
+ </RootThemeProvider>
194
+ );
195
+ expect(document.documentElement.style.colorScheme).toBe("light");
196
+ });
197
+ });
198
+
199
+ describe("diff optimization integrity", () => {
200
+ it("RootThemeProvider internal ThemeProvider only sets diff from base", () => {
201
+ const lightStyles = createInlineStyles(defaultLightTheme);
202
+ const lightTheme = getThemeColors(null, "light", defaultPalette);
203
+ const actualStyles = createInlineStyles(lightTheme);
204
+
205
+ // Count expected diff (theme from getThemeColors vs defaultLightTheme)
206
+ let expectedDiffCount = 0;
207
+ for (const key in actualStyles) {
208
+ if (actualStyles[key] !== lightStyles[key]) {
209
+ expectedDiffCount++;
210
+ }
211
+ }
212
+
213
+ const {container} = render(
214
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
215
+ <span>content</span>
216
+ </RootThemeProvider>
217
+ );
218
+
219
+ // Get the ThemeProvider's div style
220
+ const divs = Array.from(container.querySelectorAll("div")).filter((d) => d.hasAttribute("style"));
221
+ if (divs.length > 0) {
222
+ const style = divs[0].getAttribute("style") || "";
223
+ const actualCount = style.split(";").filter((s) => s.includes(":")).length;
224
+ // Actual diff should match expected diff count (or be close)
225
+ expect(actualCount).toBeLessThanOrEqual(expectedDiffCount + 5);
226
+ }
227
+ });
228
+
229
+ it("deeply nested ThemeProviders accumulate correct diffs", () => {
230
+ const {container} = render(
231
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
232
+ <ThemeProvider mode="dark">
233
+ <ThemeProvider mode="light">
234
+ <span data-testid="deepest">deep</span>
235
+ </ThemeProvider>
236
+ </ThemeProvider>
237
+ </RootThemeProvider>
238
+ );
239
+
240
+ // Each transition (light->dark, dark->light) should produce diffs
241
+ const divs = Array.from(container.querySelectorAll("div")).filter((d) => d.hasAttribute("style"));
242
+ // Should have multiple styled divs (root's ThemeProvider + nested ones)
243
+ expect(divs.length).toBeGreaterThanOrEqual(2);
244
+ });
245
+ });
246
+
247
+ describe("setPalette", () => {
248
+ it("setPalette updates theme colors", () => {
249
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
250
+
251
+ function ThemeCapture() {
252
+ const {setPalette} = useThematic();
253
+ const theme = useTheme();
254
+ capturedSetPalette = setPalette;
255
+ return <div data-testid="capture" data-card-bg={theme.cardBg} />;
256
+ }
257
+
258
+ render(
259
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
260
+ <ThemeCapture />
261
+ </RootThemeProvider>
262
+ );
263
+
264
+ const initialCardBg = screen.getByTestId("capture").getAttribute("data-card-bg");
265
+
266
+ // cardBg in light mode = white, so modify white
267
+ const newPalette = {...defaultPalette, key: "test-palette", white: "oklch(0.9 0.05 200)"};
268
+
269
+ act(() => capturedSetPalette(newPalette));
270
+
271
+ const updatedCardBg = screen.getByTestId("capture").getAttribute("data-card-bg");
272
+ expect(updatedCardBg).toBe("oklch(0.9 0.05 200)");
273
+ expect(updatedCardBg).not.toBe(initialCardBg);
274
+ });
275
+
276
+ it("nested ThemeProvider inherits updated palette", () => {
277
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
278
+
279
+ function PaletteControl() {
280
+ const {setPalette} = useThematic();
281
+ capturedSetPalette = setPalette;
282
+ return null;
283
+ }
284
+
285
+ function NestedTheme() {
286
+ const theme = useTheme();
287
+ return <div data-testid="nested" data-card-bg={theme.cardBg} />;
288
+ }
289
+
290
+ render(
291
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
292
+ <PaletteControl />
293
+ <ThemeProvider mode="light">
294
+ <NestedTheme />
295
+ </ThemeProvider>
296
+ </RootThemeProvider>
297
+ );
298
+
299
+ const initialCardBg = screen.getByTestId("nested").getAttribute("data-card-bg");
300
+
301
+ const newPalette = {...defaultPalette, key: "test-palette-2", white: "oklch(0.8 0.02 100)"};
302
+
303
+ act(() => capturedSetPalette(newPalette));
304
+
305
+ const updatedCardBg = screen.getByTestId("nested").getAttribute("data-card-bg");
306
+ expect(updatedCardBg).toBe("oklch(0.8 0.02 100)");
307
+ expect(updatedCardBg).not.toBe(initialCardBg);
308
+ });
309
+
310
+ it("setPalette(null) resets to default palette", () => {
311
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
312
+
313
+ function ThemeCapture() {
314
+ const {setPalette} = useThematic();
315
+ const theme = useTheme();
316
+ capturedSetPalette = setPalette;
317
+ return <div data-testid="capture" data-card-bg={theme.cardBg} />;
318
+ }
319
+
320
+ render(
321
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
322
+ <ThemeCapture />
323
+ </RootThemeProvider>
324
+ );
325
+
326
+ const initialCardBg = screen.getByTestId("capture").getAttribute("data-card-bg");
327
+
328
+ const newPalette = {...defaultPalette, key: "test-override", white: "oklch(0.9 0.05 200)"};
329
+ act(() => capturedSetPalette(newPalette));
330
+ expect(screen.getByTestId("capture").getAttribute("data-card-bg")).toBe("oklch(0.9 0.05 200)");
331
+
332
+ act(() => capturedSetPalette(null));
333
+ expect(screen.getByTestId("capture").getAttribute("data-card-bg")).toBe(initialCardBg);
334
+ });
335
+
336
+ it("override palette takes precedence over warm preference", () => {
337
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
338
+
339
+ function PaletteKeyDisplay() {
340
+ const {palette, setPalette} = useThematic();
341
+ capturedSetPalette = setPalette;
342
+ return <div data-testid="palette-key" data-key={palette.key} />;
343
+ }
344
+
345
+ render(
346
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
347
+ <PaletteKeyDisplay />
348
+ </RootThemeProvider>
349
+ );
350
+
351
+ const overridePalette = {...defaultPalette, key: "custom-override"};
352
+ act(() => capturedSetPalette(overridePalette));
353
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("custom-override");
354
+
355
+ // Changing warm preference doesn't affect override
356
+ act(() => storeWarmPreference(true));
357
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("custom-override");
358
+
359
+ // Clearing override picks up current warm preference
360
+ act(() => capturedSetPalette(null));
361
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("warm-user");
362
+ });
363
+ });
364
+
365
+ describe("warm preference / isArchitect", () => {
366
+ function PaletteKeyDisplay() {
367
+ const {palette} = useThematic();
368
+ return <div data-testid="palette-key" data-key={palette.key} />;
369
+ }
370
+
371
+ it("uses default palette when warm is off and not architect", () => {
372
+ render(
373
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
374
+ <PaletteKeyDisplay />
375
+ </RootThemeProvider>
376
+ );
377
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("default");
378
+ });
379
+
380
+ it("uses warm palette when warm preference is stored", () => {
381
+ storeWarmPreference(true);
382
+ render(
383
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto">
384
+ <PaletteKeyDisplay />
385
+ </RootThemeProvider>
386
+ );
387
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("warm-user");
388
+ });
389
+
390
+ it("uses architect palette when isArchitect is true", () => {
391
+ render(
392
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto" isArchitect>
393
+ <PaletteKeyDisplay />
394
+ </RootThemeProvider>
395
+ );
396
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("slate-arch");
397
+ });
398
+
399
+ it("reacts to warm preference change at runtime", () => {
400
+ render(
401
+ <RootThemeProvider initialThemeMode="light" initialPreference="light" initialMenuPreference="auto" isArchitect>
402
+ <PaletteKeyDisplay />
403
+ </RootThemeProvider>
404
+ );
405
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("slate-arch");
406
+
407
+ act(() => storeWarmPreference(true));
408
+ expect(screen.getByTestId("palette-key").getAttribute("data-key")).toBe("warm-arch");
409
+ });
410
+ });
411
+ });