@fibery/ui-kit 1.40.4 → 1.42.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.
- package/CHANGELOG.md +18 -0
- package/eslint.config.mjs +16 -0
- package/package.json +10 -9
- package/src/a11y-color.test.ts +5 -24
- package/src/actions-menu/actions-menu-item.tsx +6 -9
- package/src/actions-menu/context-actions-menu.tsx +8 -3
- package/src/ai/model.tsx +0 -1
- package/src/ai/temperature.tsx +0 -1
- package/src/animated-height-container.tsx +3 -3
- package/src/antd/index.tsx +3 -0
- package/src/antd/input.tsx +0 -1
- package/src/antd/styles.ts +9 -9
- package/src/app-icon-with-fallback.tsx +5 -5
- package/src/app-icon-wrapper.tsx +2 -3
- package/src/app-icon.tsx +3 -3
- package/src/avatar.tsx +3 -3
- package/src/button/base-button.tsx +0 -1
- package/src/button/make-button-colors.ts +2 -2
- package/src/checkbox.tsx +1 -1
- package/src/collapsible-section.tsx +11 -1
- package/src/color-utils.test.ts +15 -25
- package/src/color-utils.ts +43 -8
- package/src/command-menu/index.tsx +1 -0
- package/src/context-menu/index.tsx +5 -5
- package/src/date-picker/contexts.ts +2 -2
- package/src/date-picker/date-range-picker-popup-content.tsx +1 -2
- package/src/date-picker/date-range-picker.tsx +29 -31
- package/src/date-picker/single-date-picker-popup-content.tsx +1 -2
- package/src/date-picker/single-date-picker.tsx +149 -124
- package/src/date-picker/types.ts +4 -4
- package/src/day-select/iso-week-day-select.tsx +2 -2
- package/src/day-select/week-day-select.tsx +2 -2
- package/src/delayed.tsx +0 -1
- package/src/design-system/alpha.test.ts +59 -0
- package/src/design-system/alpha.ts +41 -0
- package/src/design-system/animation.ts +10 -0
- package/src/design-system/colors-css.test.ts +39 -0
- package/src/design-system/colors-css.ts +25 -0
- package/src/design-system/colors-js.test.ts +232 -0
- package/src/design-system/colors-js.ts +107 -0
- package/src/design-system/colors.test.ts +74 -0
- package/src/{design-system.colors.ts → design-system/colors.ts} +133 -134
- package/src/design-system/date.ts +3 -0
- package/src/design-system/layout.ts +106 -0
- package/src/design-system/theme.test.ts +94 -0
- package/src/design-system/theme.ts +93 -0
- package/src/design-system/typography.ts +179 -0
- package/src/design-system/vars.test.ts +1679 -0
- package/src/design-system/vars.ts +52 -0
- package/src/design-system.test.ts +46 -303
- package/src/design-system.ts +25 -545
- package/src/dropdown-menu/index.tsx +5 -5
- package/src/emoji-picker/icon-emoji-picker.tsx +1 -1
- package/src/fibermoji-placeholder.tsx +3 -3
- package/src/field-container.tsx +11 -2
- package/src/file-item/file-icon.tsx +3 -3
- package/src/file-item-2.tsx +4 -10
- package/src/file-item.tsx +0 -1
- package/src/icons/ast/ChatBubble.ts +8 -0
- package/src/icons/ast/ChatFloat.ts +8 -0
- package/src/icons/ast/ChatSidebar.ts +8 -0
- package/src/icons/ast/FileOther.ts +8 -0
- package/src/icons/ast/NoBorder.ts +8 -0
- package/src/icons/ast/NoFill.ts +8 -0
- package/src/icons/ast/index.tsx +6 -0
- package/src/icons/react/ChatBubble.tsx +13 -0
- package/src/icons/react/ChatFloat.tsx +13 -0
- package/src/icons/react/ChatSidebar.tsx +13 -0
- package/src/icons/react/FileOther.tsx +13 -0
- package/src/icons/react/NoBorder.tsx +13 -0
- package/src/icons/react/NoFill.tsx +13 -0
- package/src/icons/react/index.tsx +6 -0
- package/src/icons/svg/chat-bubble.svg +4 -0
- package/src/icons/svg/chat-float.svg +4 -0
- package/src/icons/svg/chat-sidebar.svg +4 -0
- package/src/icons/svg/file-other.svg +3 -0
- package/src/icons/svg/no-border.svg +17 -0
- package/src/icons/svg/no-fill.svg +4 -0
- package/src/images-gallery/images-gallery.tsx +8 -6
- package/src/images-gallery/slide-buttons.tsx +2 -2
- package/src/is-iOS.ts +0 -1
- package/src/is-in-popup.ts +2 -1
- package/src/lists/actions-menu-row-surface.tsx +4 -4
- package/src/loading-sausage.tsx +2 -2
- package/src/media-query-utils.ts +1 -2
- package/src/mobile-keyboard-aware-popup.tsx +2 -2
- package/src/modal-menu/modal-menu-content.tsx +2 -2
- package/src/number-input/decimal.js +9 -7
- package/src/number-input/{number-inline-input-with-autosize.tsx → number-input-inline-with-autosize.tsx} +20 -41
- package/src/number-input/number-input-inline.tsx +118 -0
- package/src/number-input/{index.test.js → number-input.test.tsx} +38 -43
- package/src/number-input/number-input.tsx +63 -121
- package/src/number-input/types.ts +19 -0
- package/src/number-input/utils.ts +61 -0
- package/src/online-users.tsx +2 -2
- package/src/palette-generator.test.ts +24 -281
- package/src/palette-generator.ts +39 -45
- package/src/palettes/_.ts +72 -0
- package/src/palettes/slate-arch.colors-snapshot.test.ts +1689 -0
- package/src/palettes/slate-arch.ts +188 -0
- package/src/palettes/slate-user.colors-snapshot.test.ts +1689 -0
- package/src/palettes/slate-user.ts +187 -0
- package/src/palettes/warm-arch.colors-snapshot.test.ts +1689 -0
- package/src/palettes/warm-arch.ts +222 -0
- package/src/palettes/warm-user.colors-snapshot.test.ts +1689 -0
- package/src/palettes/warm-user.ts +222 -0
- package/src/popover/get-element-ref.ts +28 -0
- package/src/popover/index.tsx +232 -333
- package/src/popover/{mobile-popup-context.tsx → mobile-popover-context.tsx} +7 -7
- package/src/popover/mobile-popover.tsx +169 -0
- package/src/popover/modifiers.tsx +2 -2
- package/src/popover/popup-stack-context.tsx +8 -9
- package/src/root-theme-provider.test.tsx +114 -19
- package/src/scale-generator.ts +31 -22
- package/src/select/components/menu-list-virtualized.tsx +5 -3
- package/src/select/index.tsx +6 -5
- package/src/select/select.tsx +36 -38
- package/src/select/util.ts +1 -1
- package/src/static-palettes.ts +221 -11
- package/src/thematic-controls.tsx +6 -6
- package/src/thematic-scales.tsx +15 -15
- package/src/thematic-state.ts +57 -20
- package/src/thematic.tsx +36 -32
- package/src/theme-provider.test.tsx +31 -19
- package/src/theme-provider.tsx +41 -38
- package/src/theme-settings.ts +66 -11
- package/src/theme-styles.ts +53 -8
- package/src/toast/toast.tsx +1 -2
- package/src/toggle.tsx +2 -2
- package/src/tooltip.tsx +4 -3
- package/src/type-badge.tsx +4 -11
- package/src/unit/styles.ts +0 -23
- package/src/use-is-phone.tsx +7 -2
- package/src/use-on-screen-keyboard-data.tsx +2 -2
- package/src/with-data.tsx +4 -3
- package/src/workflow-progress-icon.tsx +2 -2
- package/.eslintignore +0 -3
- package/.eslintrc +0 -14
- package/src/__mocks__/createInlineTheme.js +0 -3
- package/src/__snapshots__/design-system.test.ts.snap +0 -7265
- package/src/create-inline-theme.ts +0 -67
- package/src/number-input/index.js +0 -191
- package/src/palette.ts +0 -253
package/src/thematic-state.ts
CHANGED
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
type ThematicPrefs,
|
|
11
11
|
type ThemePalette,
|
|
12
12
|
} from "./palette-generator";
|
|
13
|
-
import {defaultPalette} from "./theme-styles";
|
|
14
13
|
import {getThemePreference, setThemePreference, type ThemePreference} from "./theme-settings";
|
|
15
14
|
import {StaticPalettes} from "./static-palettes";
|
|
16
15
|
import {defaultPrefs, HIGHLIGHT_COLOR, nextThemePref} from "./thematic-constants";
|
|
16
|
+
import {useToast} from "./toast/toast-provider";
|
|
17
17
|
|
|
18
18
|
interface StoredState {
|
|
19
19
|
prefs: ThematicPrefs | null;
|
|
@@ -21,8 +21,7 @@ interface StoredState {
|
|
|
21
21
|
customPalette: Partial<ThemePalette> | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export type ThematicExport = {
|
|
25
|
-
scales: Partial<Omit<ThemePalette, "key">>;
|
|
24
|
+
export type ThematicExport = Partial<Omit<ThemePalette, "key">> & {
|
|
26
25
|
meta?: {
|
|
27
26
|
prefs: ThematicPrefs | null;
|
|
28
27
|
};
|
|
@@ -30,10 +29,25 @@ export type ThematicExport = {
|
|
|
30
29
|
|
|
31
30
|
const STORAGE_KEY = "thematic-palette";
|
|
32
31
|
|
|
32
|
+
function isValidPalette(p: Partial<ThemePalette> | null): boolean {
|
|
33
|
+
if (!p) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return Array.isArray(p.whiteA) && Array.isArray(p.blackA);
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
function loadFromStorage(): StoredState | null {
|
|
34
40
|
try {
|
|
35
41
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
36
|
-
|
|
42
|
+
if (!raw) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const parsed = JSON.parse(raw) as StoredState;
|
|
46
|
+
if (!isValidPalette(parsed.customPalette)) {
|
|
47
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
37
51
|
} catch {
|
|
38
52
|
return null;
|
|
39
53
|
}
|
|
@@ -50,8 +64,9 @@ function saveToStorage(data: StoredState) {
|
|
|
50
64
|
const stored = loadFromStorage();
|
|
51
65
|
|
|
52
66
|
export function useThematicState() {
|
|
53
|
-
const {setPalette} = useThematic();
|
|
67
|
+
const {palette: providerPalette, setPalette} = useThematic();
|
|
54
68
|
const themeMode = useThemeMode();
|
|
69
|
+
const toast = useToast();
|
|
55
70
|
|
|
56
71
|
const [themePref, setThemePref] = useState<ThemePreference>(getThemePreference);
|
|
57
72
|
const isAuto = themePref === "auto";
|
|
@@ -103,7 +118,7 @@ export function useThematicState() {
|
|
|
103
118
|
baseCurvesLightChanged || baseCurvesDarkChanged || accentCurvesLightChanged || accentCurvesDarkChanged;
|
|
104
119
|
|
|
105
120
|
const palette = useMemo(() => {
|
|
106
|
-
const base = staticPalette ? staticPalette.make() : prefs ? makeThematicPalette(prefs) :
|
|
121
|
+
const base = staticPalette ? staticPalette.make() : prefs ? makeThematicPalette(prefs) : providerPalette;
|
|
107
122
|
let result = customPalette ? {...base, ...customPalette} : base;
|
|
108
123
|
|
|
109
124
|
if (baseCurvesLightChanged) {
|
|
@@ -166,6 +181,7 @@ export function useThematicState() {
|
|
|
166
181
|
prefsPresent,
|
|
167
182
|
staticPalette,
|
|
168
183
|
customPalette,
|
|
184
|
+
providerPalette,
|
|
169
185
|
baseCurvesLight,
|
|
170
186
|
baseCurvesDark,
|
|
171
187
|
accentCurvesLight,
|
|
@@ -185,10 +201,14 @@ export function useThematicState() {
|
|
|
185
201
|
key: `${palette.key}-highlight-${highlight.scale}-${highlight.index}`,
|
|
186
202
|
[highlight.scale]: scale as unknown as ColorScale,
|
|
187
203
|
});
|
|
204
|
+
} else if (prefs !== null || staticPalette !== null || customPalette !== null || anyCurvesChanged) {
|
|
205
|
+
if (palette.key !== providerPalette.key) {
|
|
206
|
+
setPalette(palette);
|
|
207
|
+
}
|
|
188
208
|
} else {
|
|
189
|
-
setPalette(
|
|
209
|
+
setPalette(null);
|
|
190
210
|
}
|
|
191
|
-
}, [palette, highlight, setPalette]);
|
|
211
|
+
}, [palette, providerPalette, highlight, prefs, staticPalette, customPalette, anyCurvesChanged, setPalette]);
|
|
192
212
|
|
|
193
213
|
const update = (partial: Partial<ThematicPrefs>) => {
|
|
194
214
|
setStaticPaletteIndex(null);
|
|
@@ -209,7 +229,12 @@ export function useThematicState() {
|
|
|
209
229
|
|
|
210
230
|
const cycleStaticPalettes = () => {
|
|
211
231
|
setPrefs(null);
|
|
212
|
-
setStaticPaletteIndex((prev) =>
|
|
232
|
+
setStaticPaletteIndex((prev) => {
|
|
233
|
+
const next = prev === null ? 0 : (prev + 1) % StaticPalettes.length;
|
|
234
|
+
const key = StaticPalettes[next].make().key;
|
|
235
|
+
toast.info({title: key});
|
|
236
|
+
return next;
|
|
237
|
+
});
|
|
213
238
|
};
|
|
214
239
|
|
|
215
240
|
const cycleTheme = () => {
|
|
@@ -226,13 +251,17 @@ export function useThematicState() {
|
|
|
226
251
|
setBaseCurvesDark(DEFAULT_DARK_CURVES);
|
|
227
252
|
setAccentCurvesLight(DEFAULT_LIGHT_CURVES);
|
|
228
253
|
setAccentCurvesDark(DEFAULT_DARK_CURVES);
|
|
254
|
+
saveToStorage({prefs: null, staticPaletteIndex: null, customPalette: null});
|
|
229
255
|
};
|
|
230
256
|
|
|
231
257
|
const updateScaleColor = useCallback(
|
|
232
258
|
(scaleName: keyof ThemePalette, index: number, color: string) => {
|
|
233
259
|
setCustomPalette((prev) => {
|
|
234
|
-
const
|
|
235
|
-
|
|
260
|
+
const currentValue = prev?.[scaleName] ?? palette[scaleName];
|
|
261
|
+
if (typeof currentValue === "string") {
|
|
262
|
+
return {...prev, [scaleName]: color};
|
|
263
|
+
}
|
|
264
|
+
const mutableScale = [...currentValue] as unknown as string[];
|
|
236
265
|
mutableScale[index] = color;
|
|
237
266
|
return {...prev, [scaleName]: mutableScale as unknown as ColorScale};
|
|
238
267
|
});
|
|
@@ -262,28 +291,36 @@ export function useThematicState() {
|
|
|
262
291
|
const setAccentCurves = editingDark ? setAccentCurvesDark : setAccentCurvesLight;
|
|
263
292
|
|
|
264
293
|
const exportState = useCallback((): ThematicExport => {
|
|
265
|
-
const keys: (keyof Omit<ThemePalette, "key">)[] = ["white", "black"];
|
|
294
|
+
const keys: (keyof Omit<ThemePalette, "key">)[] = ["white", "whiteA", "black", "blackA"];
|
|
266
295
|
if (showLight) {
|
|
267
296
|
keys.push("base", "accent", "red", "teal", "yellow");
|
|
268
297
|
}
|
|
269
298
|
if (showDark) {
|
|
270
299
|
keys.push("baseDark", "accentDark", "redDark", "tealDark", "yellowDark");
|
|
271
300
|
}
|
|
272
|
-
const
|
|
301
|
+
const result: ThematicExport = {};
|
|
273
302
|
for (const k of keys) {
|
|
274
|
-
|
|
303
|
+
(result as Record<string, unknown>)[k] = palette[k];
|
|
275
304
|
}
|
|
276
305
|
const bothModes = showLight && showDark;
|
|
277
|
-
|
|
306
|
+
if (bothModes) {
|
|
307
|
+
result.meta = {prefs};
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
278
310
|
}, [palette, showLight, showDark, prefs]);
|
|
279
311
|
|
|
280
312
|
const importState = useCallback((data: ThematicExport) => {
|
|
281
|
-
|
|
282
|
-
|
|
313
|
+
const {meta, ...scales} = data;
|
|
314
|
+
if (!isValidPalette(scales)) {
|
|
315
|
+
alert("Outdated palette format");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (meta) {
|
|
319
|
+
setPrefs(meta.prefs);
|
|
283
320
|
setStaticPaletteIndex(null);
|
|
284
321
|
}
|
|
285
|
-
const hasLight = "base" in
|
|
286
|
-
const hasDark = "baseDark" in
|
|
322
|
+
const hasLight = "base" in scales;
|
|
323
|
+
const hasDark = "baseDark" in scales;
|
|
287
324
|
if (hasLight && !hasDark) {
|
|
288
325
|
setThemePreference("light");
|
|
289
326
|
setThemePref("light");
|
|
@@ -295,7 +332,7 @@ export function useThematicState() {
|
|
|
295
332
|
(prev) =>
|
|
296
333
|
({
|
|
297
334
|
...prev,
|
|
298
|
-
...
|
|
335
|
+
...scales,
|
|
299
336
|
key: `import-${Date.now()}`,
|
|
300
337
|
} as Partial<ThemePalette>)
|
|
301
338
|
);
|
package/src/thematic.tsx
CHANGED
|
@@ -2,9 +2,7 @@ import {css} from "@linaria/core";
|
|
|
2
2
|
import {createPortal} from "react-dom";
|
|
3
3
|
import {border, space, themeVars} from "./design-system";
|
|
4
4
|
import {ThemeProvider} from "./theme-provider";
|
|
5
|
-
import {
|
|
6
|
-
import type {ColorScale} from "./palette-generator";
|
|
7
|
-
import type {ThematicExport} from "./thematic-state";
|
|
5
|
+
import {toOKLCH, toRGB} from "./color-utils";
|
|
8
6
|
import {useThematicState} from "./thematic-state";
|
|
9
7
|
import {IconButton} from "./button/icon-button";
|
|
10
8
|
import Refresh from "./icons/react/Refresh";
|
|
@@ -17,7 +15,8 @@ import FileDownload from "./icons/react/FileDownload";
|
|
|
17
15
|
import FileUpload from "./icons/react/FileUpload";
|
|
18
16
|
import {themeIcons} from "./thematic-constants";
|
|
19
17
|
import {ArrayEditor, ArraySlider, controlRowCss, Slider} from "./thematic-controls";
|
|
20
|
-
import {ScalePair, ScaleRow, sectionSeparatorCss} from "./thematic-scales";
|
|
18
|
+
import {scaleCss, ScalePair, ScaleRow, sectionLabelCss, sectionSeparatorCss} from "./thematic-scales";
|
|
19
|
+
import {SwatchWithPicker} from "./thematic-color-picker";
|
|
21
20
|
|
|
22
21
|
const panelCss = css`
|
|
23
22
|
position: fixed;
|
|
@@ -37,18 +36,6 @@ const buttonsCss = css`
|
|
|
37
36
|
margin-top: ${space.s12}px;
|
|
38
37
|
`;
|
|
39
38
|
|
|
40
|
-
function toOklchScale(scale: ColorScale): ColorScale {
|
|
41
|
-
return scale.map((c) => produceColor(c, (x) => x, OKLCH)) as unknown as ColorScale;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function toOklchExport(data: ThematicExport): ThematicExport {
|
|
45
|
-
const scales = {...data.scales};
|
|
46
|
-
for (const [k, v] of Object.entries(scales)) {
|
|
47
|
-
(scales as Record<string, ColorScale>)[k] = toOklchScale(v);
|
|
48
|
-
}
|
|
49
|
-
return {...data, scales};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
export function Thematic() {
|
|
53
40
|
const {
|
|
54
41
|
palette,
|
|
@@ -80,18 +67,17 @@ export function Thematic() {
|
|
|
80
67
|
importState,
|
|
81
68
|
} = useThematicState();
|
|
82
69
|
|
|
83
|
-
const handleExport = (
|
|
70
|
+
const handleExport = () => {
|
|
84
71
|
const data = exportState();
|
|
85
|
-
const
|
|
86
|
-
const blob = new Blob([JSON.stringify(output, null, 2)], {type: "application/json"});
|
|
72
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], {type: "application/json"});
|
|
87
73
|
const url = URL.createObjectURL(blob);
|
|
88
74
|
const a = document.createElement("a");
|
|
89
75
|
a.href = url;
|
|
90
|
-
const hasLight = "base" in data
|
|
91
|
-
const hasDark = "baseDark" in data
|
|
76
|
+
const hasLight = "base" in data;
|
|
77
|
+
const hasDark = "baseDark" in data;
|
|
92
78
|
const modes = [hasLight && "light", hasDark && "dark"].filter(Boolean).join("&");
|
|
93
79
|
const now = new Date().toISOString().replaceAll(/\D+/g, "");
|
|
94
|
-
a.download = `palette-${modes}
|
|
80
|
+
a.download = `palette-${modes}-${now}.json`;
|
|
95
81
|
a.click();
|
|
96
82
|
URL.revokeObjectURL(url);
|
|
97
83
|
};
|
|
@@ -273,11 +259,31 @@ export function Thematic() {
|
|
|
273
259
|
|
|
274
260
|
<div className={sectionSeparatorCss} />
|
|
275
261
|
|
|
262
|
+
<div className={sectionLabelCss}>lights & shades</div>
|
|
263
|
+
<div className={scaleCss}>
|
|
264
|
+
<div style={{border: "1px solid #888", borderRadius: 5}}>
|
|
265
|
+
<SwatchWithPicker
|
|
266
|
+
color={palette.white}
|
|
267
|
+
title={`white\n${toRGB(palette.white)}\n${toOKLCH(palette.white)}`}
|
|
268
|
+
onChange={(c) => updateScaleColor("white", 0, c)}
|
|
269
|
+
isAltPressed={isAltPressed}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
<div style={{border: "1px solid #888", borderRadius: 5}}>
|
|
273
|
+
<SwatchWithPicker
|
|
274
|
+
color={palette.black}
|
|
275
|
+
title={`black\n${toRGB(palette.black)}\n${toOKLCH(palette.black)}`}
|
|
276
|
+
onChange={(c) => updateScaleColor("black", 0, c)}
|
|
277
|
+
isAltPressed={isAltPressed}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
276
282
|
<ScaleRow
|
|
277
|
-
name="
|
|
278
|
-
scale={palette.
|
|
279
|
-
onColorChange={(i, c) => updateScaleColor("
|
|
280
|
-
onHighlight={highlightScale("
|
|
283
|
+
name="whiteA"
|
|
284
|
+
scale={palette.whiteA}
|
|
285
|
+
onColorChange={(i, c) => updateScaleColor("whiteA", i, c)}
|
|
286
|
+
onHighlight={highlightScale("whiteA")}
|
|
281
287
|
onHighlightEnd={clearHighlight}
|
|
282
288
|
isAltPressed={isAltPressed}
|
|
283
289
|
/>
|
|
@@ -298,13 +304,11 @@ export function Thematic() {
|
|
|
298
304
|
onChange={(v) => update({whiteChroma: v})}
|
|
299
305
|
/>
|
|
300
306
|
|
|
301
|
-
<div className={sectionSeparatorCss} />
|
|
302
|
-
|
|
303
307
|
<ScaleRow
|
|
304
|
-
name="
|
|
305
|
-
scale={palette.
|
|
306
|
-
onColorChange={(i, c) => updateScaleColor("
|
|
307
|
-
onHighlight={highlightScale("
|
|
308
|
+
name="blackA"
|
|
309
|
+
scale={palette.blackA}
|
|
310
|
+
onColorChange={(i, c) => updateScaleColor("blackA", i, c)}
|
|
311
|
+
onHighlight={highlightScale("blackA")}
|
|
308
312
|
onHighlightEnd={clearHighlight}
|
|
309
313
|
isAltPressed={isAltPressed}
|
|
310
314
|
/>
|
|
@@ -11,12 +11,9 @@ import {
|
|
|
11
11
|
useThemeStyles,
|
|
12
12
|
} from "./theme-provider";
|
|
13
13
|
import {defaultDarkTheme, defaultLightTheme, defaultLightThemeStyles} from "./theme-styles";
|
|
14
|
-
import {
|
|
14
|
+
import {createInlineStyles, getThemeColors, themeVars} from "./design-system";
|
|
15
15
|
import {makeDefaultPalette} from "./palette-generator";
|
|
16
16
|
|
|
17
|
-
// Use real createInlineTheme, not the mock
|
|
18
|
-
jest.unmock("./create-inline-theme");
|
|
19
|
-
|
|
20
17
|
const palette = makeDefaultPalette();
|
|
21
18
|
|
|
22
19
|
function ThemeDisplay({testId = "theme-display"}: {testId?: string}) {
|
|
@@ -79,8 +76,8 @@ describe("ThemeProvider", () => {
|
|
|
79
76
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
80
77
|
|
|
81
78
|
// Compare full inline themes to verify they differ
|
|
82
|
-
const lightStyles =
|
|
83
|
-
const darkStyles =
|
|
79
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
80
|
+
const darkStyles = createInlineStyles(darkTheme);
|
|
84
81
|
|
|
85
82
|
// Count how many properties differ between light and dark
|
|
86
83
|
let diffCount = 0;
|
|
@@ -117,8 +114,8 @@ describe("ThemeProvider", () => {
|
|
|
117
114
|
const theme3 = getThemeColors(null, "light", palette); // back to light
|
|
118
115
|
|
|
119
116
|
// Verify themes differ
|
|
120
|
-
const styles1 =
|
|
121
|
-
const styles2 =
|
|
117
|
+
const styles1 = createInlineStyles(theme1);
|
|
118
|
+
const styles2 = createInlineStyles(theme2);
|
|
122
119
|
|
|
123
120
|
let diffCount = 0;
|
|
124
121
|
for (const key in styles2) {
|
|
@@ -199,8 +196,8 @@ describe("ThemeProvider", () => {
|
|
|
199
196
|
const lightTheme = getThemeColors(null, "light", palette);
|
|
200
197
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
201
198
|
|
|
202
|
-
const lightStyles =
|
|
203
|
-
const darkStyles =
|
|
199
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
200
|
+
const darkStyles = createInlineStyles(darkTheme);
|
|
204
201
|
|
|
205
202
|
let diffCount = 0;
|
|
206
203
|
for (const key in darkStyles) {
|
|
@@ -309,6 +306,21 @@ describe("ThemeProvider", () => {
|
|
|
309
306
|
// Theme should be generated with the custom color
|
|
310
307
|
expect(screen.getByTestId("theme-display")).toBeInTheDocument();
|
|
311
308
|
});
|
|
309
|
+
|
|
310
|
+
it("color={var(--...)} is a no-op (same theme as parent)", () => {
|
|
311
|
+
const {container} = render(
|
|
312
|
+
<ThemeProvider mode="light">
|
|
313
|
+
<ThemeProvider color={themeVars.accentTextColor}>
|
|
314
|
+
<ThemeDisplay />
|
|
315
|
+
</ThemeProvider>
|
|
316
|
+
</ThemeProvider>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const divs = Array.from(container.querySelectorAll("div")).filter((d) => !d.hasAttribute("data-testid"));
|
|
320
|
+
const innerDiv = divs[divs.length - 1];
|
|
321
|
+
const style = innerDiv?.getAttribute("style") || "";
|
|
322
|
+
expect(style).toBe("");
|
|
323
|
+
});
|
|
312
324
|
});
|
|
313
325
|
|
|
314
326
|
describe("style optimization verification", () => {
|
|
@@ -344,8 +356,8 @@ describe("ThemeProvider", () => {
|
|
|
344
356
|
const lightTheme = getThemeColors(null, "light", palette);
|
|
345
357
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
346
358
|
|
|
347
|
-
const lightStyles =
|
|
348
|
-
const darkStyles =
|
|
359
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
360
|
+
const darkStyles = createInlineStyles(darkTheme);
|
|
349
361
|
|
|
350
362
|
// Calculate light→dark diff
|
|
351
363
|
const lightToDarkDiff: Record<string, string> = {};
|
|
@@ -371,11 +383,11 @@ describe("ThemeProvider", () => {
|
|
|
371
383
|
const lightTheme = getThemeColors(null, "light", palette);
|
|
372
384
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
373
385
|
|
|
374
|
-
const fullStyles =
|
|
386
|
+
const fullStyles = createInlineStyles(darkTheme);
|
|
375
387
|
const fullCount = Object.keys(fullStyles).length;
|
|
376
388
|
|
|
377
389
|
// Calculate diff
|
|
378
|
-
const lightStyles =
|
|
390
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
379
391
|
let diffCount = 0;
|
|
380
392
|
for (const key in fullStyles) {
|
|
381
393
|
if (fullStyles[key] !== lightStyles[key]) {
|
|
@@ -666,8 +678,8 @@ describe("ThemeProvider", () => {
|
|
|
666
678
|
const lightTheme = getThemeColors(null, "light", palette);
|
|
667
679
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
668
680
|
|
|
669
|
-
const lightStyles =
|
|
670
|
-
const darkStyles =
|
|
681
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
682
|
+
const darkStyles = createInlineStyles(darkTheme);
|
|
671
683
|
let expectedDiffCount = 0;
|
|
672
684
|
for (const key in darkStyles) {
|
|
673
685
|
if (darkStyles[key] !== lightStyles[key]) {
|
|
@@ -710,8 +722,8 @@ describe("ThemeProvider", () => {
|
|
|
710
722
|
const lightTheme = getThemeColors(null, "light", palette);
|
|
711
723
|
const darkTheme = getThemeColors(null, "dark", palette);
|
|
712
724
|
|
|
713
|
-
const lightStyles =
|
|
714
|
-
const darkStyles =
|
|
725
|
+
const lightStyles = createInlineStyles(lightTheme);
|
|
726
|
+
const darkStyles = createInlineStyles(darkTheme);
|
|
715
727
|
let expectedDiffCount = 0;
|
|
716
728
|
for (const key in darkStyles) {
|
|
717
729
|
if (darkStyles[key] !== lightStyles[key]) {
|
|
@@ -765,7 +777,7 @@ describe("ThemeProvider", () => {
|
|
|
765
777
|
|
|
766
778
|
function StylesCheck() {
|
|
767
779
|
const styles = useThemeStyles();
|
|
768
|
-
const expected =
|
|
780
|
+
const expected = createInlineStyles(darkTheme);
|
|
769
781
|
const match = Object.keys(expected).every((k) => styles[k] === expected[k]);
|
|
770
782
|
return <div data-testid="styles-match" data-match={String(match)} />;
|
|
771
783
|
}
|
package/src/theme-provider.tsx
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import invariant from "invariant";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
defaultDarkThemeStyles,
|
|
3
|
+
chooseGlobalStyles,
|
|
5
4
|
defaultLightTheme,
|
|
6
5
|
defaultLightThemeStyles,
|
|
7
6
|
defaultPalette,
|
|
8
|
-
|
|
7
|
+
selectGlobalTheme,
|
|
9
8
|
} from "./theme-styles";
|
|
10
9
|
import {layoutStyles} from "./layout-styles";
|
|
11
10
|
import {
|
|
12
11
|
createContext,
|
|
12
|
+
forwardRef,
|
|
13
13
|
PropsWithChildren,
|
|
14
14
|
ReactNode,
|
|
15
15
|
useContext,
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
useMemo,
|
|
19
19
|
useState,
|
|
20
20
|
} from "react";
|
|
21
|
-
import {
|
|
22
|
-
import {ThemePalette} from "./palette-generator";
|
|
21
|
+
import {createInlineStyles, getThemeColors, ThemeColors, ThemeStyles} from "./design-system";
|
|
22
|
+
import {selectPalette, ThemePalette} from "./palette-generator";
|
|
23
23
|
import {
|
|
24
24
|
subscribeOnThemeMenuPreferenceChange,
|
|
25
25
|
subscribeOnThemeModeChange,
|
|
@@ -27,9 +27,9 @@ import {
|
|
|
27
27
|
ThemeMenuPreference,
|
|
28
28
|
ThemeMode,
|
|
29
29
|
ThemePreference,
|
|
30
|
+
useWarmPreference,
|
|
30
31
|
} from "./theme-settings";
|
|
31
32
|
import {css} from "@linaria/core";
|
|
32
|
-
import {ThemeStyles} from "./create-inline-theme";
|
|
33
33
|
|
|
34
34
|
const ThemePreferenceContext = createContext<ThemePreference | null>(null);
|
|
35
35
|
|
|
@@ -67,9 +67,13 @@ export function useThemeStyles(): ThemeStyles {
|
|
|
67
67
|
|
|
68
68
|
const ThemePaletteContext = createContext<ThemePalette>(defaultPalette);
|
|
69
69
|
|
|
70
|
+
export function useThemePalette(): ThemePalette {
|
|
71
|
+
return useContext(ThemePaletteContext);
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
type ThematicContextValue = {
|
|
71
75
|
palette: ThemePalette;
|
|
72
|
-
setPalette: (palette: ThemePalette) => void;
|
|
76
|
+
setPalette: (palette: ThemePalette | null) => void;
|
|
73
77
|
};
|
|
74
78
|
|
|
75
79
|
const ThematicContext = createContext<ThematicContextValue>({
|
|
@@ -101,7 +105,7 @@ export function ThemeProvider({
|
|
|
101
105
|
const theme = themeOverride ?? getThemeColors(color, mode, palette);
|
|
102
106
|
|
|
103
107
|
const parentStyles = useContext(portal ? RootThemeStylesContext : ThemeStylesContext);
|
|
104
|
-
const styles =
|
|
108
|
+
const styles = createInlineStyles(theme);
|
|
105
109
|
const stylesDiff = useMemo(() => {
|
|
106
110
|
if (styles === parentStyles) {
|
|
107
111
|
return undefined;
|
|
@@ -131,6 +135,20 @@ export function ThemeProvider({
|
|
|
131
135
|
);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Wrapper for ThemeProvider that absorbs Radix Portal's `asChild` ref/props.
|
|
140
|
+
* Use as direct child of Radix Portal components (MenuPortal, TooltipPortal, etc.)
|
|
141
|
+
*/
|
|
142
|
+
export const RadixPortalThemeProvider = forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<"div">>(
|
|
143
|
+
function PortalThemeProvider({children, ...rest}, ref) {
|
|
144
|
+
return (
|
|
145
|
+
<div ref={ref} {...rest}>
|
|
146
|
+
<ThemeProvider portal>{children}</ThemeProvider>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
134
152
|
export function MenuThemeProvider({children}: {children: ReactNode}) {
|
|
135
153
|
const themeMode = useThemeMode();
|
|
136
154
|
// light2 = always dark menu
|
|
@@ -142,31 +160,41 @@ export function RootThemeProvider({
|
|
|
142
160
|
initialThemeMode,
|
|
143
161
|
initialPreference,
|
|
144
162
|
initialMenuPreference,
|
|
163
|
+
isArchitect = false,
|
|
145
164
|
children,
|
|
146
165
|
}: {
|
|
147
166
|
initialThemeMode: ThemeMode;
|
|
148
167
|
initialPreference: ThemePreference;
|
|
149
168
|
initialMenuPreference: ThemeMenuPreference;
|
|
169
|
+
isArchitect?: boolean;
|
|
150
170
|
children: ReactNode;
|
|
151
171
|
}): JSX.Element {
|
|
152
172
|
const [themeMode, setThemeMode] = useState<ThemeMode>(initialThemeMode);
|
|
153
173
|
useEffect(() => subscribeOnThemeModeChange(setThemeMode), []);
|
|
154
174
|
|
|
155
|
-
const [
|
|
175
|
+
const [warm] = useWarmPreference();
|
|
176
|
+
useEffect(() => chooseGlobalStyles(themeMode, warm), [themeMode, warm]);
|
|
177
|
+
|
|
178
|
+
const [paletteOverride, setPaletteOverride] = useState<ThemePalette | null>(null);
|
|
179
|
+
|
|
180
|
+
const palette = useMemo(() => {
|
|
181
|
+
return paletteOverride ?? selectPalette({warmLook: warm, isArchitect});
|
|
182
|
+
}, [paletteOverride, warm, isArchitect]);
|
|
156
183
|
|
|
157
184
|
let res = <ThemeProvider theme={getThemeColors(null, themeMode, palette)}>{children}</ThemeProvider>;
|
|
158
185
|
|
|
159
186
|
// Set base context to what global CSS has, so ThemeProvider diffs against it
|
|
160
|
-
const cssThemeStyles = themeMode === "dark"
|
|
187
|
+
const [cssThemeColors, cssThemeStyles] = selectGlobalTheme(warm, themeMode === "dark");
|
|
161
188
|
res = <ThemeStylesContext.Provider value={cssThemeStyles}>{res}</ThemeStylesContext.Provider>;
|
|
162
189
|
res = <RootThemeStylesContext.Provider value={cssThemeStyles}>{res}</RootThemeStylesContext.Provider>;
|
|
163
|
-
|
|
164
|
-
const cssThemeColors = themeMode === "dark" ? defaultDarkTheme : defaultLightTheme;
|
|
165
190
|
res = <ThemeColorsContext.Provider value={cssThemeColors}>{res}</ThemeColorsContext.Provider>;
|
|
166
191
|
|
|
167
192
|
res = <ThemePaletteContext.Provider value={palette}>{res}</ThemePaletteContext.Provider>;
|
|
168
193
|
|
|
169
|
-
const thematicContextValue = useMemo<ThematicContextValue>(
|
|
194
|
+
const thematicContextValue = useMemo<ThematicContextValue>(
|
|
195
|
+
() => ({palette, setPalette: setPaletteOverride}),
|
|
196
|
+
[palette, setPaletteOverride]
|
|
197
|
+
);
|
|
170
198
|
res = <ThematicContext.Provider value={thematicContextValue}>{res}</ThematicContext.Provider>;
|
|
171
199
|
|
|
172
200
|
const [themePreference, setThemePreference] = useState<ThemePreference>(initialPreference);
|
|
@@ -185,30 +213,5 @@ export function RootThemeProvider({
|
|
|
185
213
|
};
|
|
186
214
|
}, []);
|
|
187
215
|
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
const darkThemeClassName = "dark-theme";
|
|
190
|
-
const lightThemeAndDarkMenuClassName = "light-theme-and-dark-menu";
|
|
191
|
-
const meta = document.querySelector('meta[name="color-scheme"]');
|
|
192
|
-
let colorScheme = "light";
|
|
193
|
-
const documentElement = document.documentElement;
|
|
194
|
-
documentElement.classList.remove(darkThemeClassName);
|
|
195
|
-
documentElement.classList.remove(lightThemeAndDarkMenuClassName);
|
|
196
|
-
documentElement.classList.add(themeStyles);
|
|
197
|
-
if (themeMode === "dark") {
|
|
198
|
-
documentElement.classList.add(darkThemeClassName);
|
|
199
|
-
colorScheme = "dark";
|
|
200
|
-
} else if (themeMode === "light2") {
|
|
201
|
-
documentElement.classList.add(lightThemeAndDarkMenuClassName);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
documentElement.style.colorScheme = colorScheme;
|
|
205
|
-
meta?.setAttribute("content", colorScheme);
|
|
206
|
-
|
|
207
|
-
return () => {
|
|
208
|
-
documentElement.classList.remove(darkThemeClassName);
|
|
209
|
-
documentElement.classList.remove(lightThemeAndDarkMenuClassName);
|
|
210
|
-
};
|
|
211
|
-
}, [themeMode]);
|
|
212
|
-
|
|
213
216
|
return res;
|
|
214
217
|
}
|
package/src/theme-settings.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {useEffect, useState} from "react";
|
|
1
2
|
import {getMediaQueryList, subscribeOnChange as subscribeOnMediaQueryChange} from "./media-query-utils";
|
|
2
3
|
|
|
3
4
|
const listenersThemeMode = new Map();
|
|
@@ -10,6 +11,7 @@ const themeMenuPreferenceKey = "theme-menu-preference";
|
|
|
10
11
|
export type ThemePreference = "dark" | "light" | "auto";
|
|
11
12
|
export type ThemeMenuPreference = "dark" | "auto";
|
|
12
13
|
|
|
14
|
+
export const themeModes = ["dark", "light", "light2"] as const;
|
|
13
15
|
export type ThemeMode = "dark" | "light" | "light2";
|
|
14
16
|
|
|
15
17
|
export function getThemePreference(): ThemePreference {
|
|
@@ -56,22 +58,18 @@ function getDarkMediaQuery() {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function getThemeMode(): ThemeMode {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const matches = getDarkMediaQuery().matches;
|
|
64
|
-
if (matches) {
|
|
61
|
+
const item = getThemePreference();
|
|
62
|
+
const menu = getThemeMenuPreference();
|
|
63
|
+
if (item === "auto") {
|
|
64
|
+
if (getDarkMediaQuery().matches) {
|
|
65
65
|
return "dark";
|
|
66
|
-
}
|
|
67
|
-
if (menuPreference === "dark") {
|
|
66
|
+
} else if (menu === "dark") {
|
|
68
67
|
return "light2";
|
|
69
68
|
}
|
|
70
69
|
return "light";
|
|
71
|
-
} else if (
|
|
70
|
+
} else if (item === "dark") {
|
|
72
71
|
return "dark";
|
|
73
|
-
}
|
|
74
|
-
if (menuPreference === "dark") {
|
|
72
|
+
} else if (menu === "dark") {
|
|
75
73
|
return "light2";
|
|
76
74
|
}
|
|
77
75
|
return "light";
|
|
@@ -171,3 +169,60 @@ export function subscribeOnThemeModeChange(cb: (themeMode: ThemeMode) => void):
|
|
|
171
169
|
window.removeEventListener("storage", storageListener);
|
|
172
170
|
};
|
|
173
171
|
}
|
|
172
|
+
|
|
173
|
+
const warmKey = "warm";
|
|
174
|
+
const warmListeners = new Set<(warm: boolean) => void>();
|
|
175
|
+
|
|
176
|
+
export function hasWarmPreference(): boolean {
|
|
177
|
+
try {
|
|
178
|
+
return localStorage.getItem(warmKey) !== null;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function loadWarmPreference(): boolean {
|
|
185
|
+
try {
|
|
186
|
+
return localStorage.getItem(warmKey) === "true";
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function storeWarmPreference(warm: boolean) {
|
|
193
|
+
try {
|
|
194
|
+
localStorage.setItem(warmKey, JSON.stringify(warm));
|
|
195
|
+
} catch {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
warmListeners.forEach((cb) => cb(warm));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function subscribeOnWarmPreferenceChange(cb: (warm: boolean) => void): () => void {
|
|
202
|
+
warmListeners.add(cb);
|
|
203
|
+
let prev = loadWarmPreference();
|
|
204
|
+
const storageListener = () => {
|
|
205
|
+
const next = loadWarmPreference();
|
|
206
|
+
if (prev !== next) {
|
|
207
|
+
prev = next;
|
|
208
|
+
cb(next);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
window.addEventListener("storage", storageListener);
|
|
212
|
+
return () => {
|
|
213
|
+
warmListeners.delete(cb);
|
|
214
|
+
window.removeEventListener("storage", storageListener);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function useWarmPreference() {
|
|
219
|
+
const [warm, setWarm] = useState(loadWarmPreference);
|
|
220
|
+
useEffect(() => subscribeOnWarmPreferenceChange(setWarm), []);
|
|
221
|
+
return [
|
|
222
|
+
warm,
|
|
223
|
+
(warm: boolean) => {
|
|
224
|
+
setWarm(warm);
|
|
225
|
+
storeWarmPreference(warm);
|
|
226
|
+
},
|
|
227
|
+
] as const;
|
|
228
|
+
}
|