@fibery/ui-kit 1.40.4 → 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 (141) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/eslint.config.mjs +16 -0
  3. package/package.json +8 -7
  4. package/src/a11y-color.test.ts +5 -24
  5. package/src/actions-menu/actions-menu-item.tsx +6 -9
  6. package/src/actions-menu/context-actions-menu.tsx +8 -3
  7. package/src/ai/model.tsx +0 -1
  8. package/src/ai/temperature.tsx +0 -1
  9. package/src/animated-height-container.tsx +3 -3
  10. package/src/antd/index.tsx +3 -0
  11. package/src/antd/input.tsx +0 -1
  12. package/src/antd/styles.ts +9 -9
  13. package/src/app-icon-with-fallback.tsx +5 -5
  14. package/src/app-icon-wrapper.tsx +2 -3
  15. package/src/app-icon.tsx +3 -3
  16. package/src/avatar.tsx +3 -3
  17. package/src/button/base-button.tsx +0 -1
  18. package/src/button/make-button-colors.ts +2 -2
  19. package/src/checkbox.tsx +1 -1
  20. package/src/collapsible-section.tsx +11 -1
  21. package/src/color-utils.test.ts +15 -25
  22. package/src/color-utils.ts +43 -8
  23. package/src/command-menu/index.tsx +1 -0
  24. package/src/context-menu/index.tsx +5 -5
  25. package/src/date-picker/contexts.ts +2 -2
  26. package/src/date-picker/date-range-picker.tsx +3 -4
  27. package/src/date-picker/single-date-picker.tsx +147 -119
  28. package/src/date-picker/types.ts +4 -3
  29. package/src/day-select/iso-week-day-select.tsx +2 -2
  30. package/src/day-select/week-day-select.tsx +2 -2
  31. package/src/delayed.tsx +0 -1
  32. package/src/design-system/alpha.test.ts +59 -0
  33. package/src/design-system/alpha.ts +41 -0
  34. package/src/design-system/animation.ts +10 -0
  35. package/src/design-system/colors-css.test.ts +39 -0
  36. package/src/design-system/colors-css.ts +25 -0
  37. package/src/design-system/colors-js.test.ts +232 -0
  38. package/src/design-system/colors-js.ts +107 -0
  39. package/src/design-system/colors.test.ts +74 -0
  40. package/src/{design-system.colors.ts → design-system/colors.ts} +133 -134
  41. package/src/design-system/date.ts +3 -0
  42. package/src/design-system/layout.ts +106 -0
  43. package/src/design-system/theme.test.ts +94 -0
  44. package/src/design-system/theme.ts +93 -0
  45. package/src/design-system/typography.ts +179 -0
  46. package/src/design-system/vars.test.ts +1679 -0
  47. package/src/design-system/vars.ts +52 -0
  48. package/src/design-system.test.ts +46 -303
  49. package/src/design-system.ts +25 -545
  50. package/src/dropdown-menu/index.tsx +5 -5
  51. package/src/emoji-picker/icon-emoji-picker.tsx +1 -1
  52. package/src/fibermoji-placeholder.tsx +3 -3
  53. package/src/field-container.tsx +11 -2
  54. package/src/file-item/file-icon.tsx +3 -3
  55. package/src/file-item-2.tsx +4 -10
  56. package/src/file-item.tsx +0 -1
  57. package/src/icons/ast/ChatBubble.ts +8 -0
  58. package/src/icons/ast/ChatFloat.ts +8 -0
  59. package/src/icons/ast/ChatSidebar.ts +8 -0
  60. package/src/icons/ast/FileOther.ts +8 -0
  61. package/src/icons/ast/NoBorder.ts +8 -0
  62. package/src/icons/ast/NoFill.ts +8 -0
  63. package/src/icons/ast/index.tsx +6 -0
  64. package/src/icons/react/ChatBubble.tsx +13 -0
  65. package/src/icons/react/ChatFloat.tsx +13 -0
  66. package/src/icons/react/ChatSidebar.tsx +13 -0
  67. package/src/icons/react/FileOther.tsx +13 -0
  68. package/src/icons/react/NoBorder.tsx +13 -0
  69. package/src/icons/react/NoFill.tsx +13 -0
  70. package/src/icons/react/index.tsx +6 -0
  71. package/src/icons/svg/chat-bubble.svg +4 -0
  72. package/src/icons/svg/chat-float.svg +4 -0
  73. package/src/icons/svg/chat-sidebar.svg +4 -0
  74. package/src/icons/svg/file-other.svg +3 -0
  75. package/src/icons/svg/no-border.svg +17 -0
  76. package/src/icons/svg/no-fill.svg +4 -0
  77. package/src/images-gallery/images-gallery.tsx +8 -6
  78. package/src/images-gallery/slide-buttons.tsx +2 -2
  79. package/src/is-iOS.ts +0 -1
  80. package/src/is-in-popup.ts +2 -1
  81. package/src/lists/actions-menu-row-surface.tsx +4 -4
  82. package/src/loading-sausage.tsx +2 -2
  83. package/src/media-query-utils.ts +1 -2
  84. package/src/mobile-keyboard-aware-popup.tsx +2 -2
  85. package/src/modal-menu/modal-menu-content.tsx +2 -2
  86. package/src/number-input/decimal.js +9 -7
  87. package/src/number-input/{number-inline-input-with-autosize.tsx → number-input-inline-with-autosize.tsx} +20 -41
  88. package/src/number-input/number-input-inline.tsx +118 -0
  89. package/src/number-input/{index.test.js → number-input.test.tsx} +38 -43
  90. package/src/number-input/number-input.tsx +63 -121
  91. package/src/number-input/types.ts +19 -0
  92. package/src/number-input/utils.ts +61 -0
  93. package/src/online-users.tsx +2 -2
  94. package/src/palette-generator.test.ts +24 -281
  95. package/src/palette-generator.ts +39 -45
  96. package/src/palettes/_.ts +72 -0
  97. package/src/palettes/slate-arch.colors-snapshot.test.ts +1689 -0
  98. package/src/palettes/slate-arch.ts +188 -0
  99. package/src/palettes/slate-user.colors-snapshot.test.ts +1689 -0
  100. package/src/palettes/slate-user.ts +187 -0
  101. package/src/palettes/warm-arch.colors-snapshot.test.ts +1689 -0
  102. package/src/palettes/warm-arch.ts +222 -0
  103. package/src/palettes/warm-user.colors-snapshot.test.ts +1689 -0
  104. package/src/palettes/warm-user.ts +222 -0
  105. package/src/popover/get-element-ref.ts +28 -0
  106. package/src/popover/index.tsx +232 -333
  107. package/src/popover/{mobile-popup-context.tsx → mobile-popover-context.tsx} +7 -7
  108. package/src/popover/mobile-popover.tsx +169 -0
  109. package/src/popover/modifiers.tsx +2 -2
  110. package/src/popover/popup-stack-context.tsx +8 -9
  111. package/src/root-theme-provider.test.tsx +114 -19
  112. package/src/scale-generator.ts +31 -22
  113. package/src/select/components/menu-list-virtualized.tsx +5 -3
  114. package/src/select/index.tsx +6 -5
  115. package/src/select/select.tsx +36 -38
  116. package/src/select/util.ts +1 -1
  117. package/src/static-palettes.ts +221 -11
  118. package/src/thematic-controls.tsx +6 -6
  119. package/src/thematic-scales.tsx +15 -15
  120. package/src/thematic-state.ts +57 -20
  121. package/src/thematic.tsx +36 -32
  122. package/src/theme-provider.test.tsx +31 -19
  123. package/src/theme-provider.tsx +41 -38
  124. package/src/theme-settings.ts +66 -11
  125. package/src/theme-styles.ts +53 -8
  126. package/src/toast/toast.tsx +1 -2
  127. package/src/toggle.tsx +2 -2
  128. package/src/tooltip.tsx +4 -3
  129. package/src/type-badge.tsx +4 -11
  130. package/src/unit/styles.ts +0 -23
  131. package/src/use-is-phone.tsx +7 -2
  132. package/src/use-on-screen-keyboard-data.tsx +2 -2
  133. package/src/with-data.tsx +4 -3
  134. package/src/workflow-progress-icon.tsx +2 -2
  135. package/.eslintignore +0 -3
  136. package/.eslintrc +0 -14
  137. package/src/__mocks__/createInlineTheme.js +0 -3
  138. package/src/__snapshots__/design-system.test.ts.snap +0 -7265
  139. package/src/create-inline-theme.ts +0 -67
  140. package/src/number-input/index.js +0 -191
  141. package/src/palette.ts +0 -253
@@ -1,20 +1,20 @@
1
- import _ from "lodash";
1
+ import noop from "lodash/noop";
2
2
  import {FC, ReactNode, useState, useMemo} from "react";
3
3
  import {createContext} from "@fibery/react/src/create-context";
4
4
 
5
- type MobilePopupContext = {
5
+ type MobilePopoverContext = {
6
6
  title?: string;
7
7
  headerSlot?: ReactNode;
8
8
  setTitle: (title: string) => void;
9
9
  setHeader: (header?: ReactNode) => void;
10
10
  };
11
11
 
12
- const [Provider, useMobilePopup] = createContext<MobilePopupContext>("MobilePopupContext", {
13
- setTitle: _.noop,
14
- setHeader: _.noop,
12
+ const [Provider, useMobilePopover] = createContext<MobilePopoverContext>("MobilePopoverContext", {
13
+ setTitle: noop,
14
+ setHeader: noop,
15
15
  });
16
16
 
17
- export const MobilePopupProvider: FC<{children?: ReactNode}> = ({children}) => {
17
+ export const MobilePopoverProvider: FC<{children?: ReactNode}> = ({children}) => {
18
18
  const [title, setTitle] = useState<string | undefined>();
19
19
  const [headerSlot, setHeader] = useState<ReactNode | undefined>();
20
20
 
@@ -26,4 +26,4 @@ export const MobilePopupProvider: FC<{children?: ReactNode}> = ({children}) => {
26
26
  return <Provider value={contextValue}>{children}</Provider>;
27
27
  };
28
28
 
29
- export {useMobilePopup};
29
+ export {useMobilePopover};
@@ -0,0 +1,169 @@
1
+ import {stopPropagation} from "@fibery/react/src/stop-propagation";
2
+ import {css} from "@linaria/core";
3
+ import cx from "classnames";
4
+ import {CSSProperties, ReactNode} from "react";
5
+ import {
6
+ safeAreaInsetBottomVar,
7
+ safeAreaInsetLeftVar,
8
+ safeAreaInsetRightVar,
9
+ safeAreaInsetTopVar,
10
+ } from "../mobile-styles";
11
+ import {space, textStyles, themeVars, transition} from "../design-system";
12
+ import {IconButton} from "../button/icon-button";
13
+ import CloseIcon from "../icons/react/Close";
14
+ import ArrowLeftIcon from "../icons/react/ArrowLeft";
15
+ import {ThemeProvider} from "../theme-provider";
16
+ import {useMobilePopover} from "./mobile-popover-context";
17
+
18
+ const mobileAnimationClassName = css`
19
+ transition: transform ${transition}, opacity ${transition};
20
+ transform: translateY(0);
21
+ opacity: 1;
22
+
23
+ @media (prefers-reduced-motion: no-preference) {
24
+ @starting-style {
25
+ transform: translateY(20%);
26
+ opacity: 0;
27
+ }
28
+ }
29
+ `;
30
+
31
+ const popupContentClassName = css`
32
+ display: grid;
33
+ background: ${themeVars.actionMenuBg};
34
+ grid-template-rows: 60px 1fr;
35
+ `;
36
+
37
+ export const mobileZIndex = 1002;
38
+
39
+ export function MobilePopover({
40
+ content,
41
+ mobilePopupStyle,
42
+ mobilePopupContentClassName,
43
+ title,
44
+ onClose,
45
+ height,
46
+ onNavigateBack,
47
+ }: {
48
+ content: ReactNode;
49
+ mobilePopupStyle?: CSSProperties;
50
+ mobilePopupContentClassName?: string;
51
+ title: string;
52
+ height?: string;
53
+ onClose?: () => void;
54
+ onNavigateBack?: () => void;
55
+ }) {
56
+ const {title: contextTitle, headerSlot} = useMobilePopover();
57
+
58
+ const headerContent = headerSlot ?? (
59
+ <>
60
+ {onNavigateBack && (
61
+ <IconButton onClick={onNavigateBack} size="xLarge">
62
+ <ArrowLeftIcon />
63
+ </IconButton>
64
+ )}
65
+ <div
66
+ className={css`
67
+ ${textStyles.heading3}
68
+ flex: 1;
69
+ min-width: 0;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ `}
74
+ >
75
+ {contextTitle ?? title}
76
+ </div>
77
+ {onClose && (
78
+ <IconButton onClick={onClose} size="xLarge">
79
+ <CloseIcon />
80
+ </IconButton>
81
+ )}
82
+ </>
83
+ );
84
+
85
+ const popupContent = (
86
+ <>
87
+ <div
88
+ className={cx(
89
+ css`
90
+ display: flex;
91
+ align-items: center;
92
+ gap: ${space.s8}px;
93
+ padding: ${space.s8}px ${space.s8}px ${space.s8}px ${space.s20}px;
94
+ min-width: 0;
95
+ `,
96
+ onNavigateBack &&
97
+ css`
98
+ padding-left: ${space.s4}px;
99
+ `
100
+ )}
101
+ >
102
+ {headerContent}
103
+ </div>
104
+ <div
105
+ className={cx(
106
+ css`
107
+ border-top: ${themeVars.separators.opacity5};
108
+ min-width: 0;
109
+ `,
110
+ mobilePopupContentClassName
111
+ )}
112
+ >
113
+ {content}
114
+ </div>
115
+ </>
116
+ );
117
+
118
+ const res = (
119
+ <div
120
+ className={cx(
121
+ css`
122
+ position: absolute;
123
+ left: var(${safeAreaInsetLeftVar});
124
+ right: var(${safeAreaInsetRightVar});
125
+ top: var(${safeAreaInsetTopVar});
126
+ bottom: var(${safeAreaInsetBottomVar});
127
+ z-index: ${mobileZIndex};
128
+ `,
129
+ !height && mobileAnimationClassName,
130
+ !height && popupContentClassName,
131
+ height &&
132
+ css`
133
+ display: grid;
134
+ `
135
+ )}
136
+ style={{
137
+ ...mobilePopupStyle,
138
+ ...(height ? {gridTemplateRows: `1fr ${height}`} : {}),
139
+ }}
140
+ onClick={stopPropagation}
141
+ >
142
+ {height ? (
143
+ <>
144
+ <div
145
+ onClick={onClose}
146
+ className={css`
147
+ background-color: ${themeVars.modalBg};
148
+ `}
149
+ />
150
+ <div
151
+ className={cx(
152
+ css`
153
+ display: grid;
154
+ grid-template-rows: 60px 1fr;
155
+ `,
156
+ mobileAnimationClassName,
157
+ popupContentClassName
158
+ )}
159
+ >
160
+ {popupContent}
161
+ </div>
162
+ </>
163
+ ) : (
164
+ popupContent
165
+ )}
166
+ </div>
167
+ );
168
+ return <ThemeProvider portal>{res}</ThemeProvider>;
169
+ }
@@ -3,7 +3,7 @@ import {Modifier, ModifierArguments, Placement, Boundary, RootBoundary, Padding,
3
3
  import maxSize from "popper-max-size-modifier";
4
4
  import {getResizeListener} from "@fibery/helpers/utils/resize-listener";
5
5
  import {isElementReference} from "./reference";
6
- import _ from "lodash";
6
+ import noop from "lodash/noop";
7
7
 
8
8
  type MaxSizeOptions = {
9
9
  placement: Placement;
@@ -65,6 +65,6 @@ export const observeWidth: Modifier<"observeWidth", Record<string, never>> = {
65
65
  name: "observeWidth",
66
66
  enabled: true,
67
67
  phase: "main",
68
- fn: _.noop,
68
+ fn: noop,
69
69
  effect: observeWidthEffect,
70
70
  };
@@ -25,17 +25,16 @@ const [Provider, usePopupStackContext] = createContext<{
25
25
 
26
26
  export function PopupStackContextProvider({children}: {children: ReactNode}) {
27
27
  useEffect(() => {
28
- const escapeHandler = (event: KeyboardEvent) => {
29
- const reverse = [...callbacksMap].reverse();
30
- reverse.forEach(([_id, cb]) => {
31
- if (cb) {
32
- cb(event);
33
- }
34
- });
28
+ const onKeyDown = (event: KeyboardEvent) => {
29
+ if (event.key === "Escape") {
30
+ const callbacks = [...callbacksMap.values()].filter(Boolean);
31
+ const topmost = callbacks[callbacks.length - 1];
32
+ topmost?.(event);
33
+ }
35
34
  };
36
- document.addEventListener("keydown", escapeHandler);
35
+ document.addEventListener("keydown", onKeyDown);
37
36
  return () => {
38
- document.removeEventListener("keydown", escapeHandler);
37
+ document.removeEventListener("keydown", onKeyDown);
39
38
  };
40
39
  }, []);
41
40
  return <Provider value={value}>{children}</Provider>;
@@ -8,13 +8,10 @@ import {
8
8
  useThemeMode,
9
9
  useThemePreference,
10
10
  } from "./theme-provider";
11
- import {makeDefaultPalette, ThemePalette} from "./palette-generator";
12
- import {defaultDarkTheme, defaultLightTheme} from "./theme-styles";
13
- import {createInlineTheme, getThemeColors} from "./design-system";
14
-
15
- jest.unmock("./create-inline-theme");
16
-
17
- const palette = makeDefaultPalette();
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";
18
15
 
19
16
  function ThemeDisplay({testId = "theme-display"}: {testId?: string}) {
20
17
  const theme = useTheme();
@@ -49,6 +46,7 @@ describe("RootThemeProvider", () => {
49
46
  beforeEach(() => {
50
47
  document.documentElement.className = "";
51
48
  document.documentElement.style.colorScheme = "";
49
+ localStorage.clear();
52
50
  });
53
51
 
54
52
  describe("context provision", () => {
@@ -200,9 +198,9 @@ describe("RootThemeProvider", () => {
200
198
 
201
199
  describe("diff optimization integrity", () => {
202
200
  it("RootThemeProvider internal ThemeProvider only sets diff from base", () => {
203
- const lightStyles = createInlineTheme(defaultLightTheme, []);
204
- const lightTheme = getThemeColors(null, "light", palette);
205
- const actualStyles = createInlineTheme(lightTheme, []);
201
+ const lightStyles = createInlineStyles(defaultLightTheme);
202
+ const lightTheme = getThemeColors(null, "light", defaultPalette);
203
+ const actualStyles = createInlineStyles(lightTheme);
206
204
 
207
205
  // Count expected diff (theme from getThemeColors vs defaultLightTheme)
208
206
  let expectedDiffCount = 0;
@@ -248,7 +246,7 @@ describe("RootThemeProvider", () => {
248
246
 
249
247
  describe("setPalette", () => {
250
248
  it("setPalette updates theme colors", () => {
251
- let capturedSetPalette: (p: ThemePalette) => void = () => {};
249
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
252
250
 
253
251
  function ThemeCapture() {
254
252
  const {setPalette} = useThematic();
@@ -265,10 +263,8 @@ describe("RootThemeProvider", () => {
265
263
 
266
264
  const initialCardBg = screen.getByTestId("capture").getAttribute("data-card-bg");
267
265
 
268
- // cardBg in light mode = white[0], so modify white scale
269
- const newPalette = makeDefaultPalette();
270
- newPalette.key = "test-palette"; // must change key - getThemeColors is memoized by key
271
- newPalette.white = {...newPalette.white, 0: "oklch(0.9 0.05 200)"};
266
+ // cardBg in light mode = white, so modify white
267
+ const newPalette = {...defaultPalette, key: "test-palette", white: "oklch(0.9 0.05 200)"};
272
268
 
273
269
  act(() => capturedSetPalette(newPalette));
274
270
 
@@ -278,7 +274,7 @@ describe("RootThemeProvider", () => {
278
274
  });
279
275
 
280
276
  it("nested ThemeProvider inherits updated palette", () => {
281
- let capturedSetPalette: (p: ThemePalette) => void = () => {};
277
+ let capturedSetPalette: (p: ThemePalette | null) => void = () => {};
282
278
 
283
279
  function PaletteControl() {
284
280
  const {setPalette} = useThematic();
@@ -302,9 +298,7 @@ describe("RootThemeProvider", () => {
302
298
 
303
299
  const initialCardBg = screen.getByTestId("nested").getAttribute("data-card-bg");
304
300
 
305
- const newPalette = makeDefaultPalette();
306
- newPalette.key = "test-palette-2";
307
- newPalette.white = {...newPalette.white, 0: "oklch(0.8 0.02 100)"};
301
+ const newPalette = {...defaultPalette, key: "test-palette-2", white: "oklch(0.8 0.02 100)"};
308
302
 
309
303
  act(() => capturedSetPalette(newPalette));
310
304
 
@@ -312,5 +306,106 @@ describe("RootThemeProvider", () => {
312
306
  expect(updatedCardBg).toBe("oklch(0.8 0.02 100)");
313
307
  expect(updatedCardBg).not.toBe(initialCardBg);
314
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
+ });
315
410
  });
316
411
  });
@@ -1,8 +1,8 @@
1
1
  import {fromOKLCH} from "./color-utils";
2
2
  import type {ColorScale} from "./palette-generator";
3
3
 
4
- export type BasedScale12<T> = readonly [
5
- base00: T,
4
+ export type OneBasedScale<T> = readonly [
5
+ base00: null,
6
6
  step01: T,
7
7
  step02: T,
8
8
  step03: T,
@@ -17,7 +17,7 @@ export type BasedScale12<T> = readonly [
17
17
  step12: T
18
18
  ];
19
19
 
20
- export type Scale12<T> = readonly [
20
+ export type ZeroBasedScale<T> = readonly [
21
21
  step01: T,
22
22
  step02: T,
23
23
  step03: T,
@@ -32,9 +32,13 @@ export type Scale12<T> = readonly [
32
32
  step12: T
33
33
  ];
34
34
 
35
+ export function zeroBasedFromOneBased<T>(scale: OneBasedScale<T>) {
36
+ return scale.slice(1) as unknown as ZeroBasedScale<T>;
37
+ }
38
+
35
39
  export interface ScaleCurves {
36
- lightness: Scale12<number>; // 0-100 scale
37
- chroma: Scale12<number>; // multipliers relative to peakChroma
40
+ lightness: ZeroBasedScale<number>; // 0-100 scale
41
+ chroma: ZeroBasedScale<number>; // multipliers relative to peakChroma
38
42
  }
39
43
 
40
44
  export interface ScaleConfig {
@@ -155,7 +159,7 @@ function applyContrast(L: number, contrast: number): number {
155
159
  }
156
160
 
157
161
  export function generateColorScale(config: ScaleConfig): ColorScale {
158
- const {hue, chroma, mode, curves, contrast = 1, base = "hsl(0, 0%, 100%)"} = config;
162
+ const {hue, chroma, mode, curves, contrast = 1} = config;
159
163
 
160
164
  const defaultCurves = mode === "light" ? DEFAULT_LIGHT_CURVES : DEFAULT_DARK_CURVES;
161
165
  const {lightness, chroma: chromaCurve} = curves ?? defaultCurves;
@@ -166,7 +170,7 @@ export function generateColorScale(config: ScaleConfig): ColorScale {
166
170
  return fromOKLCH(adjustedL / 100, C, hue);
167
171
  });
168
172
 
169
- return [base, ...steps] as unknown as ColorScale;
173
+ return [null, ...steps] as unknown as ColorScale;
170
174
  }
171
175
 
172
176
  /**
@@ -202,12 +206,10 @@ export function generateGrayScale(config: {
202
206
  chroma?: number;
203
207
  mode: "light" | "dark";
204
208
  contrast?: number;
205
- base?: string;
206
209
  }): ColorScale {
207
- const {hue = 0, chroma = 0, mode, contrast, base} = config;
210
+ const {hue = 0, chroma = 0, mode, contrast} = config;
208
211
  const curves = mode === "light" ? GRAY_CURVES : GRAY_DARK_CURVES;
209
- const defaultBase = mode === "dark" ? "hsl(0, 0%, 5%)" : undefined;
210
- return generateColorScale({hue, chroma, mode, contrast, curves, base: base ?? defaultBase});
212
+ return generateColorScale({hue, chroma, mode, contrast, curves});
211
213
  }
212
214
 
213
215
  // ============ HUE INTERPOLATION ============
@@ -329,19 +331,26 @@ export function paletteFromHueBoth(hue: number): {light: ColorScale; dark: Color
329
331
  };
330
332
  }
331
333
 
332
- const WHITE_ALPHAS: BasedScale12<number> = [1, 0, 0.01, 0.03, 0.06, 0.09, 0.12, 0.18, 0.25, 0.39, 0.45, 0.59, 0.92];
333
- const BLACK_ALPHAS: BasedScale12<number> = [1, 0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.14, 0.22, 0.44, 0.48, 0.57, 0.91];
334
-
335
- function alphaScale(L: number, C: number, H: number, alphas: BasedScale12<number>): ColorScale {
336
- return alphas.map((a) => fromOKLCH(L, C, H, a)) as {length: 13; [i: number]: string} as ColorScale;
337
- }
338
-
339
- export function generateWhiteScale(hue: number, chroma: number, luminosity = 1, chromaMul = 1): ColorScale {
334
+ export function generateWhites(hue: number, chroma: number, luminosity = 1, chromaMul = 1) {
340
335
  const c = chroma * 0.1 * chromaMul;
341
- return alphaScale((1 - chroma * 0.1) * luminosity, c, hue, WHITE_ALPHAS);
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
+ };
342
344
  }
343
345
 
344
- export function generateBlackScale(hue: number, chroma: number, luminosity = 1, chromaMul = 1): ColorScale {
346
+ export function generateBlacks(hue: number, chroma: number, luminosity = 1, chromaMul = 1) {
345
347
  const c = chroma * 0.1 * chromaMul;
346
- return alphaScale(chroma * 0.1 * 4 * luminosity, c, hue, BLACK_ALPHAS);
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
+ };
347
356
  }
@@ -1,4 +1,6 @@
1
- import _, {isFunction, isObject} from "lodash";
1
+ import compact from "lodash/compact";
2
+ import isFunction from "lodash/isFunction";
3
+ import isObject from "lodash/isObject";
2
4
  import {
3
5
  Children,
4
6
  ComponentType,
@@ -37,14 +39,14 @@ const flattenGroupChildren: (
37
39
  children: ReactNode,
38
40
  renderGroup: (Component: ComponentType<GroupHeadingProps>, props: GroupProps["headingProps"]) => ReactNode
39
41
  ) => ReactNode[] = (children, renderGroup) => {
40
- return _.compact(
42
+ return compact(
41
43
  Children.toArray(children)
42
44
  .map((child) => {
43
45
  if (isObject(child) && "props" in child && isObject(child.props)) {
44
46
  const {props} = child;
45
47
  if (isGroupChild(props)) {
46
48
  //well, it's a group
47
- const children = _.compact(Children.toArray(props.children));
49
+ const children = compact(Children.toArray(props.children));
48
50
  if (!children.length) {
49
51
  // do not render empty groups
50
52
  return [];
@@ -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
  );