@phreshos/react-ui 0.1.35 → 0.1.37
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/README.md +147 -167
- package/dist/accordion.d.ts +7 -6
- package/dist/accordion.js +5 -5
- package/dist/alert-dialog.js +3 -1
- package/dist/alert.d.ts +17 -0
- package/dist/alert.js +30 -0
- package/dist/app-layout.d.ts +13 -0
- package/dist/app-layout.js +84 -0
- package/dist/button.d.ts +12 -24
- package/dist/button.js +34 -39
- package/dist/calendar-layout.d.ts +9 -5
- package/dist/calendar-layout.js +67 -78
- package/dist/calendar-trigger.d.ts +4 -4
- package/dist/calendar-trigger.js +7 -29
- package/dist/calendar.d.ts +2 -2
- package/dist/calendar.js +6 -5
- package/dist/checkbox.d.ts +4 -4
- package/dist/checkbox.js +16 -6
- package/dist/color-area.d.ts +20 -0
- package/dist/color-area.js +30 -0
- package/dist/color-field.d.ts +17 -0
- package/dist/color-field.js +17 -0
- package/dist/color-picker.d.ts +24 -0
- package/dist/color-picker.js +30 -0
- package/dist/color-slider.d.ts +19 -0
- package/dist/color-slider.js +29 -0
- package/dist/color-swatch-picker.d.ts +23 -0
- package/dist/color-swatch-picker.js +36 -0
- package/dist/color-swatch.d.ts +18 -0
- package/dist/color-swatch.js +20 -0
- package/dist/combo-box.d.ts +18 -10
- package/dist/combo-box.js +43 -58
- package/dist/context-menu.d.ts +14 -10
- package/dist/context-menu.js +7 -6
- package/dist/control/color-thumb.d.ts +13 -0
- package/dist/control/color-thumb.js +33 -0
- package/dist/control/color-value.d.ts +6 -0
- package/dist/control/color-value.js +7 -0
- package/dist/control/control.d.ts +60 -0
- package/dist/control/control.js +71 -0
- package/dist/control/field-button.d.ts +13 -0
- package/dist/control/field-button.js +28 -0
- package/dist/control/field.d.ts +20 -0
- package/dist/control/field.js +34 -0
- package/dist/control/icon.d.ts +11 -0
- package/dist/control/icon.js +8 -0
- package/dist/control/item.d.ts +34 -0
- package/dist/control/item.js +53 -0
- package/dist/control/open-state.d.ts +25 -0
- package/dist/control/open-state.js +8 -0
- package/dist/control/selection-track.d.ts +39 -0
- package/dist/control/selection-track.js +122 -0
- package/dist/control/selection.d.ts +44 -0
- package/dist/{selection.js → control/selection.js} +16 -0
- package/dist/control/surface-render.d.ts +10 -0
- package/dist/control/surface-render.js +12 -0
- package/dist/control/text-control.d.ts +32 -0
- package/dist/control/text-control.js +31 -0
- package/dist/control/toggle.d.ts +32 -0
- package/dist/control/toggle.js +80 -0
- package/dist/date-control.d.ts +9 -9
- package/dist/date-control.js +31 -34
- package/dist/date-field.d.ts +7 -6
- package/dist/date-field.js +8 -6
- package/dist/date-picker.d.ts +13 -6
- package/dist/date-picker.js +9 -6
- package/dist/date-range-picker.d.ts +15 -6
- package/dist/date-range-picker.js +12 -9
- package/dist/dialog.d.ts +22 -8
- package/dist/dialog.js +37 -28
- package/dist/disclosure.d.ts +8 -7
- package/dist/disclosure.js +31 -52
- package/dist/drop-zone.d.ts +18 -0
- package/dist/drop-zone.js +32 -0
- package/dist/dropdown-menu.d.ts +7 -4
- package/dist/dropdown-menu.js +5 -4
- package/dist/fieldset.d.ts +13 -0
- package/dist/fieldset.js +26 -0
- package/dist/file-trigger.d.ts +13 -0
- package/dist/file-trigger.js +6 -0
- package/dist/flex.d.ts +1 -1
- package/dist/flex.js +5 -4
- package/dist/foundation/appearance.d.ts +83 -0
- package/dist/foundation/appearance.js +38 -0
- package/dist/{aria-direction.js → foundation/aria-direction.js} +3 -1
- package/dist/foundation/color.d.ts +61 -0
- package/dist/foundation/color.js +247 -0
- package/dist/{layout.d.ts → foundation/layout.d.ts} +6 -4
- package/dist/{layout.js → foundation/layout.js} +7 -5
- package/dist/foundation/motion-style.d.ts +9 -0
- package/dist/foundation/motion-style.js +50 -0
- package/dist/foundation/preferences.d.ts +15 -0
- package/dist/{ui-provider.d.ts → foundation/provider.d.ts} +3 -5
- package/dist/{ui-provider.js → foundation/provider.js} +14 -26
- package/dist/{radius.d.ts → foundation/radius.d.ts} +6 -3
- package/dist/foundation/radius.js +11 -0
- package/dist/foundation/spacing.d.ts +11 -0
- package/dist/foundation/spacing.js +4 -0
- package/dist/foundation/visual.d.ts +42 -0
- package/dist/{appearance-context.js → foundation/visual.js} +41 -27
- package/dist/grid.d.ts +1 -1
- package/dist/grid.js +5 -4
- package/dist/icons/main.d.ts +5 -3
- package/dist/icons/main.js +5 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +9 -7
- package/dist/list-box.d.ts +24 -19
- package/dist/list-box.js +52 -109
- package/dist/main.d.ts +27 -16
- package/dist/main.js +22 -9
- package/dist/menu.d.ts +28 -32
- package/dist/menu.js +47 -93
- package/dist/number-field.d.ts +17 -0
- package/dist/number-field.js +28 -0
- package/dist/panel.d.ts +10 -5
- package/dist/panel.js +33 -12
- package/dist/popover.d.ts +19 -5
- package/dist/popover.js +22 -15
- package/dist/progress-bar.d.ts +8 -10
- package/dist/progress-bar.js +18 -34
- package/dist/radio.d.ts +15 -12
- package/dist/radio.js +27 -37
- package/dist/range-calendar.d.ts +5 -3
- package/dist/range-calendar.js +8 -7
- package/dist/scroll-area.d.ts +2 -2
- package/dist/scroll-area.js +10 -9
- package/dist/segmented-control.d.ts +23 -0
- package/dist/segmented-control.js +38 -0
- package/dist/select.d.ts +40 -13
- package/dist/select.js +63 -18
- package/dist/slider.d.ts +7 -3
- package/dist/slider.js +51 -50
- package/dist/spinner.d.ts +5 -8
- package/dist/spinner.js +12 -25
- package/dist/surface/grain.d.ts +12 -0
- package/dist/surface/grain.js +65 -0
- package/dist/{material-options.d.ts → surface/material-options.d.ts} +1 -2
- package/dist/{material-options.js → surface/material-options.js} +2 -2
- package/dist/surface/paint-class.d.ts +6 -0
- package/dist/surface/paint-class.js +36 -0
- package/dist/{shadow-options.d.ts → surface/shadow-options.d.ts} +13 -1
- package/dist/{shadow-options.js → surface/shadow-options.js} +17 -2
- package/dist/surface/surface.d.ts +73 -0
- package/dist/surface/surface.js +230 -0
- package/dist/switch.d.ts +4 -5
- package/dist/switch.js +17 -8
- package/dist/table.d.ts +29 -32
- package/dist/table.js +68 -106
- package/dist/tabs.d.ts +16 -15
- package/dist/tabs.js +44 -86
- package/dist/textarea.d.ts +2 -2
- package/dist/textarea.js +10 -14
- package/dist/time-field.d.ts +6 -6
- package/dist/time-field.js +8 -6
- package/dist/toolbar.d.ts +6 -4
- package/dist/toolbar.js +16 -15
- package/dist/tooltip.d.ts +13 -4
- package/dist/tooltip.js +17 -15
- package/dist/tree.d.ts +22 -27
- package/dist/tree.js +54 -99
- package/dist/window.d.ts +2 -2
- package/dist/window.js +18 -20
- package/package.json +3 -3
- package/dist/appearance-context.d.ts +0 -54
- package/dist/appearance.d.ts +0 -11
- package/dist/appearance.js +0 -11
- package/dist/color.d.ts +0 -69
- package/dist/color.js +0 -192
- package/dist/control-surface.d.ts +0 -20
- package/dist/control-surface.js +0 -11
- package/dist/control.d.ts +0 -70
- package/dist/control.js +0 -105
- package/dist/field-style.d.ts +0 -3
- package/dist/field-style.js +0 -12
- package/dist/material-paint.d.ts +0 -13
- package/dist/material-paint.js +0 -119
- package/dist/motion-style.d.ts +0 -13
- package/dist/motion-style.js +0 -75
- package/dist/preferences.d.ts +0 -12
- package/dist/radius.js +0 -11
- package/dist/readiness.d.ts +0 -37
- package/dist/readiness.js +0 -134
- package/dist/selection.d.ts +0 -18
- package/dist/spacing.d.ts +0 -8
- package/dist/spacing.js +0 -8
- package/dist/surface-edge.d.ts +0 -11
- package/dist/surface-edge.js +0 -55
- package/dist/surface.d.ts +0 -27
- package/dist/surface.js +0 -92
- package/dist/text-control.d.ts +0 -10
- package/dist/text-control.js +0 -0
- package/dist/toggle-indicator.d.ts +0 -18
- package/dist/toggle-indicator.js +0 -49
- /package/dist/{aria-direction.d.ts → foundation/aria-direction.d.ts} +0 -0
- /package/dist/{direction.d.ts → foundation/direction.d.ts} +0 -0
- /package/dist/{direction.js → foundation/direction.js} +0 -0
- /package/dist/{overlay-placement.d.ts → foundation/overlay-placement.d.ts} +0 -0
- /package/dist/{overlay-placement.js → foundation/overlay-placement.js} +0 -0
- /package/dist/{preferences.js → foundation/preferences.js} +0 -0
- /package/dist/{scale.d.ts → foundation/scale.d.ts} +0 -0
- /package/dist/{scale.js → foundation/scale.js} +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import ColorSpace from "colorjs.io/src/ColorSpace.js";
|
|
3
|
+
import contrastAPCA from "colorjs.io/src/contrast/APCA.js";
|
|
4
|
+
import { mix } from "colorjs.io/src/interpolation.js";
|
|
5
|
+
import parse from "colorjs.io/src/parse.js";
|
|
6
|
+
import serialize from "colorjs.io/src/serialize.js";
|
|
7
|
+
import to from "colorjs.io/src/to.js";
|
|
8
|
+
import toGamut from "colorjs.io/src/toGamut.js";
|
|
9
|
+
import sRGB from "colorjs.io/src/spaces/srgb.js";
|
|
10
|
+
import sRGB_Linear from "colorjs.io/src/spaces/srgb-linear.js";
|
|
11
|
+
import HSL from "colorjs.io/src/spaces/hsl.js";
|
|
12
|
+
import HWB from "colorjs.io/src/spaces/hwb.js";
|
|
13
|
+
import Lab from "colorjs.io/src/spaces/lab.js";
|
|
14
|
+
import LCH from "colorjs.io/src/spaces/lch.js";
|
|
15
|
+
import OKLab from "colorjs.io/src/spaces/oklab.js";
|
|
16
|
+
import OKLCH from "colorjs.io/src/spaces/oklch.js";
|
|
17
|
+
import P3 from "colorjs.io/src/spaces/p3.js";
|
|
18
|
+
import A98RGB from "colorjs.io/src/spaces/a98rgb.js";
|
|
19
|
+
import ProPhoto from "colorjs.io/src/spaces/prophoto.js";
|
|
20
|
+
import REC_2020 from "colorjs.io/src/spaces/rec2020.js";
|
|
21
|
+
import XYZ_D50 from "colorjs.io/src/spaces/xyz-d50.js";
|
|
22
|
+
import XYZ_D65 from "colorjs.io/src/spaces/xyz-d65.js";
|
|
23
|
+
import { useVisual } from "./visual.js";
|
|
24
|
+
// Register the CSS color spaces without bundling unrelated color-model APIs.
|
|
25
|
+
for (const space of [sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65])
|
|
26
|
+
ColorSpace.register(space);
|
|
27
|
+
/**
|
|
28
|
+
* Levels move a color toward the canvas (`subtle`, `soft`) or toward the
|
|
29
|
+
* content color (`strong`, `intense`). Direction therefore follows the colors
|
|
30
|
+
* themselves, so every level keeps its meaning in any Theme.
|
|
31
|
+
*/
|
|
32
|
+
const levels = {
|
|
33
|
+
subtle: { toward: "background", amount: 0.84 },
|
|
34
|
+
soft: { toward: "background", amount: 0.62 },
|
|
35
|
+
strong: { toward: "foreground", amount: 0.22 },
|
|
36
|
+
intense: { toward: "foreground", amount: 0.42 }
|
|
37
|
+
};
|
|
38
|
+
/*
|
|
39
|
+
* Every derivation below is pure and repeats constantly across components and
|
|
40
|
+
* renders. Results are cached by their inputs so a theme change or a hover
|
|
41
|
+
* costs a lookup instead of a color-space conversion.
|
|
42
|
+
*/
|
|
43
|
+
const concreteCache = new Map();
|
|
44
|
+
const luminanceCache = new Map();
|
|
45
|
+
const mixCache = new Map();
|
|
46
|
+
const resolvedCache = new WeakMap();
|
|
47
|
+
const readableCache = new WeakMap();
|
|
48
|
+
const cacheLimit = 2_048;
|
|
49
|
+
function remember(cache, key, value) {
|
|
50
|
+
if (cache.size >= cacheLimit)
|
|
51
|
+
cache.delete(cache.keys().next().value);
|
|
52
|
+
cache.set(key, value);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
/** Returns an opaque sRGB serialization, or `null` when the value is not a concrete CSS color. */
|
|
56
|
+
export function concreteColor(value) {
|
|
57
|
+
const cached = concreteCache.get(value);
|
|
58
|
+
if (cached !== undefined)
|
|
59
|
+
return cached;
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = serialize(toGamut(to({ ...parse(value), alpha: 1 }, "srgb")), { format: "hex" });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
result = null;
|
|
66
|
+
}
|
|
67
|
+
return remember(concreteCache, value, result);
|
|
68
|
+
}
|
|
69
|
+
/** WCAG relative luminance of a concrete color, or `null` when it cannot be evaluated. */
|
|
70
|
+
export function luminance(value) {
|
|
71
|
+
const cached = luminanceCache.get(value);
|
|
72
|
+
if (cached !== undefined)
|
|
73
|
+
return cached;
|
|
74
|
+
const concrete = concreteColor(value);
|
|
75
|
+
let result = null;
|
|
76
|
+
if (concrete !== null) {
|
|
77
|
+
const [red, green, blue] = to(parse(concrete), "srgb-linear").coords.map(channel => channel ?? 0);
|
|
78
|
+
result = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
|
79
|
+
}
|
|
80
|
+
return remember(luminanceCache, value, result);
|
|
81
|
+
}
|
|
82
|
+
/** WCAG contrast ratio between two colors, or `null` when either cannot be evaluated. */
|
|
83
|
+
export function contrast(first, second) {
|
|
84
|
+
const a = luminance(first);
|
|
85
|
+
const b = luminance(second);
|
|
86
|
+
if (a === null || b === null)
|
|
87
|
+
return null;
|
|
88
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Mixes `amount` of `target` into `base` in OKLab, concretely when possible.
|
|
92
|
+
* OKLab keeps the hue while moving toward a near-neutral canvas or text color;
|
|
93
|
+
* OKLCH would rotate the hue toward the neutral's meaningless angle.
|
|
94
|
+
*/
|
|
95
|
+
export function mixColor(base, target, amount) {
|
|
96
|
+
if (amount <= 0)
|
|
97
|
+
return base;
|
|
98
|
+
const key = `${base}|${target}|${amount}`;
|
|
99
|
+
const cached = mixCache.get(key);
|
|
100
|
+
if (cached !== undefined)
|
|
101
|
+
return cached;
|
|
102
|
+
const first = concreteColor(base);
|
|
103
|
+
const second = concreteColor(target);
|
|
104
|
+
const result = first !== null && second !== null
|
|
105
|
+
? concreteColor(serialize(mix(first, second, amount, { space: "oklab" })))
|
|
106
|
+
: `color-mix(in oklab, ${base}, ${target} ${Math.round(amount * 10_000) / 100}%)`;
|
|
107
|
+
return remember(mixCache, key, result);
|
|
108
|
+
}
|
|
109
|
+
/** Applies transparency without restricting the source CSS color syntax. */
|
|
110
|
+
export function colorOpacity(value, opacity) {
|
|
111
|
+
return `color-mix(in srgb, ${value} ${Math.round(opacity * 10_000) / 100}%, transparent)`;
|
|
112
|
+
}
|
|
113
|
+
/** Resolves one Color against the active Appearance colors. */
|
|
114
|
+
export function resolveColor(value, colors) {
|
|
115
|
+
let cache = resolvedCache.get(colors);
|
|
116
|
+
if (cache === undefined)
|
|
117
|
+
resolvedCache.set(colors, cache = new Map());
|
|
118
|
+
const cached = cache.get(value);
|
|
119
|
+
if (cached !== undefined)
|
|
120
|
+
return cached;
|
|
121
|
+
const separator = value.indexOf(":");
|
|
122
|
+
const name = separator < 0 ? value : value.slice(0, separator);
|
|
123
|
+
const level = separator < 0 ? "base" : value.slice(separator + 1);
|
|
124
|
+
const source = Object.hasOwn(colors, name) ? colors[name] : undefined;
|
|
125
|
+
const result = source === undefined || !isColorLevel(level)
|
|
126
|
+
? value
|
|
127
|
+
: level === "base" ? source : colorLevel(source, level, colors);
|
|
128
|
+
cache.set(value, result);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/** Derives one level from a concrete base color. */
|
|
132
|
+
export function colorLevel(base, level, colors) {
|
|
133
|
+
if (level === "base")
|
|
134
|
+
return base;
|
|
135
|
+
const { toward, amount } = levels[level];
|
|
136
|
+
return mixColor(base, colors[toward], amount);
|
|
137
|
+
}
|
|
138
|
+
/** Every level derived from one base color. */
|
|
139
|
+
export function colorScale(base, colors) {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
subtle: colorLevel(base, "subtle", colors),
|
|
142
|
+
soft: colorLevel(base, "soft", colors),
|
|
143
|
+
base,
|
|
144
|
+
strong: colorLevel(base, "strong", colors),
|
|
145
|
+
intense: colorLevel(base, "intense", colors)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Perceived lightness contrast (APCA Lc magnitude) of `text` drawn on `fill`,
|
|
150
|
+
* or `null` when either cannot be evaluated. Unlike the WCAG 2 ratio, it is
|
|
151
|
+
* polarity-aware: the WCAG ratio overrates dark text on saturated mid-tone
|
|
152
|
+
* fills such as blue or green, where light text actually reads better.
|
|
153
|
+
*/
|
|
154
|
+
export function readability(fill, text) {
|
|
155
|
+
const background = concreteColor(fill);
|
|
156
|
+
const foreground = concreteColor(text);
|
|
157
|
+
if (background === null || foreground === null)
|
|
158
|
+
return null;
|
|
159
|
+
return Math.abs(contrastAPCA(background, foreground));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* APCA grades readability in steps of 15 Lc. Contrasts closer than one step
|
|
163
|
+
* read alike, so they are not a reason to leave the Appearance text color.
|
|
164
|
+
*/
|
|
165
|
+
const readabilityStep = 15;
|
|
166
|
+
/**
|
|
167
|
+
* Returns the Appearance text color, `foreground`, unless `background` reads
|
|
168
|
+
* clearly better on the fill: better by at least one APCA step.
|
|
169
|
+
*/
|
|
170
|
+
export function readableColor(fill, colors) {
|
|
171
|
+
let cache = readableCache.get(colors);
|
|
172
|
+
if (cache === undefined)
|
|
173
|
+
readableCache.set(colors, cache = new Map());
|
|
174
|
+
const cached = cache.get(fill);
|
|
175
|
+
if (cached !== undefined)
|
|
176
|
+
return cached;
|
|
177
|
+
const onBackground = readability(fill, colors.background);
|
|
178
|
+
const onForeground = readability(fill, colors.foreground);
|
|
179
|
+
const result = onBackground === null || onForeground === null
|
|
180
|
+
? colors.foreground
|
|
181
|
+
: onBackground >= onForeground + readabilityStep ? colors.background : colors.foreground;
|
|
182
|
+
cache.set(fill, result);
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/** Whether the canvas is darker than its content, derived from the colors themselves. */
|
|
186
|
+
export function darkCanvas(colors) {
|
|
187
|
+
const background = luminance(colors.background);
|
|
188
|
+
const foreground = luminance(colors.foreground);
|
|
189
|
+
return background !== null && foreground !== null && background < foreground;
|
|
190
|
+
}
|
|
191
|
+
const darknessCache = new WeakMap();
|
|
192
|
+
/**
|
|
193
|
+
* How dark the canvas is, from 0 (a light canvas) to 1 (a dark canvas), read
|
|
194
|
+
* from the perceptual lightness of the background itself. Treatments that
|
|
195
|
+
* depend on the canvas interpolate along it instead of switching at a threshold,
|
|
196
|
+
* so a mid-tone canvas receives a mid-tone treatment.
|
|
197
|
+
*/
|
|
198
|
+
export function canvasDarkness(colors) {
|
|
199
|
+
const cached = darknessCache.get(colors);
|
|
200
|
+
if (cached !== undefined)
|
|
201
|
+
return cached;
|
|
202
|
+
const concrete = concreteColor(colors.background);
|
|
203
|
+
const lightness = concrete === null ? (darkCanvas(colors) ? 0 : 1) : to(parse(concrete), "oklch").coords[0] ?? 1;
|
|
204
|
+
const progress = Math.min(1, Math.max(0, (0.82 - lightness) / (0.82 - 0.28)));
|
|
205
|
+
// Smoothstep keeps both ends settled and the middle continuous.
|
|
206
|
+
const result = progress * progress * (3 - 2 * progress);
|
|
207
|
+
darknessCache.set(colors, result);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
const recessCache = new Map();
|
|
211
|
+
/**
|
|
212
|
+
* The paint of a recess in `base`: the same color moved just far enough toward
|
|
213
|
+
* whichever extreme has more room to reach one constant perceptual difference.
|
|
214
|
+
* Depth therefore reads the same on a light, mid-tone, or dark canvas.
|
|
215
|
+
*/
|
|
216
|
+
export function recessColor(base) {
|
|
217
|
+
const cached = recessCache.get(base);
|
|
218
|
+
if (cached !== undefined)
|
|
219
|
+
return cached;
|
|
220
|
+
const toward = (contrast(base, "#ffffff") ?? 1) > (contrast(base, "#000000") ?? 1) ? "#ffffff" : "#000000";
|
|
221
|
+
let result = base;
|
|
222
|
+
for (let amount = 0.005; amount <= 0.25; amount += 0.005) {
|
|
223
|
+
result = mixColor(base, toward, Math.round(amount * 1_000) / 1_000);
|
|
224
|
+
if ((contrast(base, result) ?? recessContrast) >= recessContrast)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
return remember(recessCache, base, result);
|
|
228
|
+
}
|
|
229
|
+
/** The constant difference between a recess and the paint it sinks into. */
|
|
230
|
+
const recessContrast = 1.085;
|
|
231
|
+
/** The lighter Appearance content candidate: the paint of anything lit from above, such as a thumb. */
|
|
232
|
+
export function lightColor(colors) {
|
|
233
|
+
return darkCanvas(colors) ? colors.foreground : colors.default;
|
|
234
|
+
}
|
|
235
|
+
export function isColorLevel(value) {
|
|
236
|
+
return value === "subtle" || value === "soft" || value === "base" || value === "strong" || value === "intense";
|
|
237
|
+
}
|
|
238
|
+
/** Every level derived from one color in the active Appearance. */
|
|
239
|
+
export function useColor(value) {
|
|
240
|
+
const { colors } = useVisual();
|
|
241
|
+
return useMemo(() => colorScale(resolveColor(value, colors), colors), [value, colors]);
|
|
242
|
+
}
|
|
243
|
+
/** The readable content color for one fill in the active Appearance. */
|
|
244
|
+
export function useContrastingColor(value) {
|
|
245
|
+
const { colors } = useVisual();
|
|
246
|
+
return useMemo(() => readableColor(resolveColor(value, colors), colors), [value, colors]);
|
|
247
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
import { type Spacing } from "./spacing.js";
|
|
2
|
+
import type { Spacing } from "./spacing.js";
|
|
4
3
|
/** Cross-axis alignment shared by layout containers. */
|
|
5
4
|
export type LayoutAlignment = "start" | "center" | "end" | "stretch" | "baseline";
|
|
6
5
|
/** Main-axis distribution shared by layout containers. */
|
|
@@ -9,6 +8,9 @@ export type LayoutJustification = "start" | "center" | "end" | "between" | "arou
|
|
|
9
8
|
export type LayoutGap = Spacing;
|
|
10
9
|
export declare function alignment(value: LayoutAlignment | undefined): CSSProperties["alignItems"] | undefined;
|
|
11
10
|
export declare function justification(value: LayoutJustification | undefined): CSSProperties["justifyContent"] | undefined;
|
|
12
|
-
/** Resolves semantic spacing while preserving explicit CSS gap values. */
|
|
13
|
-
export declare function resolveGap(value: LayoutGap | undefined, appearance: Appearance | null): CSSProperties["gap"];
|
|
14
11
|
export declare function tracks(value: number | string | undefined, property: "columns" | "rows"): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The height of a shell header row, shared by Window and Panel: a 16px icon
|
|
14
|
+
* or line of title plus the Appearance spacing above and below.
|
|
15
|
+
*/
|
|
16
|
+
export declare function headerHeight(spacing: number): number;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { resolveSpacing } from "./spacing.js";
|
|
2
1
|
const alignments = {
|
|
3
2
|
start: "flex-start",
|
|
4
3
|
center: "center",
|
|
@@ -20,10 +19,6 @@ export function alignment(value) {
|
|
|
20
19
|
export function justification(value) {
|
|
21
20
|
return value === undefined ? undefined : justifications[value];
|
|
22
21
|
}
|
|
23
|
-
/** Resolves semantic spacing while preserving explicit CSS gap values. */
|
|
24
|
-
export function resolveGap(value, appearance) {
|
|
25
|
-
return resolveSpacing(value, appearance);
|
|
26
|
-
}
|
|
27
22
|
export function tracks(value, property) {
|
|
28
23
|
if (value === undefined || typeof value === "string")
|
|
29
24
|
return value;
|
|
@@ -32,3 +27,10 @@ export function tracks(value, property) {
|
|
|
32
27
|
}
|
|
33
28
|
return `repeat(${value}, minmax(0, 1fr))`;
|
|
34
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The height of a shell header row, shared by Window and Panel: a 16px icon
|
|
32
|
+
* or line of title plus the Appearance spacing above and below.
|
|
33
|
+
*/
|
|
34
|
+
export function headerHeight(spacing) {
|
|
35
|
+
return 16 + spacing * 2;
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { Visual } from "./visual.js";
|
|
3
|
+
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
4
|
+
export declare function overlayTransition(visual: Visual): CSSProperties;
|
|
5
|
+
export declare const overlayMotionClass = "phreshos-ui-overlay";
|
|
6
|
+
export declare const backdropMotionClass = "phreshos-ui-backdrop";
|
|
7
|
+
/** React hoists and deduplicates this stylesheet, including inside portals. */
|
|
8
|
+
export default function MotionStyle(): import("react").JSX.Element;
|
|
9
|
+
export declare const textControlClass = "phreshos-ui-text-control";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
3
|
+
export function overlayTransition(visual) {
|
|
4
|
+
return {
|
|
5
|
+
"--phreshos-ui-motion-duration": `${visual.duration}ms`,
|
|
6
|
+
"--phreshos-ui-motion-easing": visual.easing
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export const overlayMotionClass = "phreshos-ui-overlay";
|
|
10
|
+
export const backdropMotionClass = "phreshos-ui-backdrop";
|
|
11
|
+
const stylesheet = `
|
|
12
|
+
.phreshos-ui-overlay[data-entering] {
|
|
13
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) both;
|
|
14
|
+
}
|
|
15
|
+
.phreshos-ui-overlay[data-exiting] {
|
|
16
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) reverse both;
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
}
|
|
19
|
+
.phreshos-ui-backdrop[data-entering] {
|
|
20
|
+
animation: phreshos-ui-backdrop-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) both;
|
|
21
|
+
}
|
|
22
|
+
.phreshos-ui-backdrop[data-exiting] {
|
|
23
|
+
animation: phreshos-ui-backdrop-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) reverse both;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
@keyframes phreshos-ui-overlay-enter {
|
|
27
|
+
from { opacity: 0; scale: 1.05; }
|
|
28
|
+
to { opacity: 1; scale: 1; }
|
|
29
|
+
}
|
|
30
|
+
@keyframes phreshos-ui-backdrop-enter {
|
|
31
|
+
from { opacity: 0; }
|
|
32
|
+
to { opacity: 1; }
|
|
33
|
+
}
|
|
34
|
+
.phreshos-ui-text-control::placeholder {
|
|
35
|
+
color: inherit;
|
|
36
|
+
opacity: 0.55;
|
|
37
|
+
}
|
|
38
|
+
@keyframes phreshos-ui-spin {
|
|
39
|
+
to { rotate: 360deg; }
|
|
40
|
+
}
|
|
41
|
+
@keyframes phreshos-ui-sweep {
|
|
42
|
+
from { inset-inline-start: -40%; }
|
|
43
|
+
to { inset-inline-start: 100%; }
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
/** React hoists and deduplicates this stylesheet, including inside portals. */
|
|
47
|
+
export default function MotionStyle() {
|
|
48
|
+
return _jsx("style", { href: "phreshos-react-ui-motion", precedence: "phreshos", children: stylesheet });
|
|
49
|
+
}
|
|
50
|
+
export const textControlClass = "phreshos-ui-text-control";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A visual Theme selects one branch of every themed Appearance value. */
|
|
2
|
+
export type Theme = "light" | "dark";
|
|
3
|
+
/** Complete environment preferences consumed by React UI. */
|
|
4
|
+
export type Preferences = Readonly<{
|
|
5
|
+
theme: Theme;
|
|
6
|
+
animations: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
/** At least one preference merged with the nearest UIProvider. */
|
|
9
|
+
export type PreferencesUpdate = Readonly<{
|
|
10
|
+
theme: Theme;
|
|
11
|
+
animations?: boolean;
|
|
12
|
+
}> | Readonly<{
|
|
13
|
+
theme?: Theme;
|
|
14
|
+
animations: boolean;
|
|
15
|
+
}>;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { type AppearanceUpdate } from "./appearance.js";
|
|
3
3
|
import { type Direction } from "./direction.js";
|
|
4
4
|
import type { PreferencesUpdate } from "./preferences.js";
|
|
5
|
-
export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
|
|
6
|
-
export { useDirection, useDocumentDirection } from "./direction.js";
|
|
7
|
-
/** Establishes only the explicitly supplied UI values for a React subtree. */
|
|
8
|
-
export declare function UIProvider({ appearance, children, direction, preferences }: UIProviderProps): ReactNode;
|
|
9
5
|
export interface UIProviderProps {
|
|
10
6
|
readonly appearance?: AppearanceUpdate;
|
|
11
7
|
readonly children: ReactNode;
|
|
@@ -13,3 +9,5 @@ export interface UIProviderProps {
|
|
|
13
9
|
readonly direction?: Direction;
|
|
14
10
|
readonly preferences?: PreferencesUpdate;
|
|
15
11
|
}
|
|
12
|
+
/** Establishes only the explicitly supplied UI values for a React subtree. */
|
|
13
|
+
export declare function UIProvider({ appearance, children, direction, preferences }: UIProviderProps): import("react").JSX.Element;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { LucideProvider } from "lucide-react";
|
|
2
3
|
import { useMemo } from "react";
|
|
3
|
-
import {
|
|
4
|
+
import { mergeAppearance } from "./appearance.js";
|
|
4
5
|
import { DirectionContext, fixedDirectionSource } from "./direction.js";
|
|
5
|
-
|
|
6
|
-
export { useDirection, useDocumentDirection } from "./direction.js";
|
|
6
|
+
import { AppearanceContext, fixedPreferencesSource, PreferencesContext, useAppearance, usePreferences } from "./visual.js";
|
|
7
7
|
const directionBoundaryStyle = { display: "contents" };
|
|
8
8
|
/** Establishes only the explicitly supplied UI values for a React subtree. */
|
|
9
9
|
export function UIProvider({ appearance, children, direction, preferences }) {
|
|
@@ -16,8 +16,14 @@ export function UIProvider({ appearance, children, direction, preferences }) {
|
|
|
16
16
|
if (directionSource !== undefined) {
|
|
17
17
|
subtree = _jsx(DirectionContext.Provider, { value: directionSource, children: _jsx("div", { dir: direction, style: directionBoundaryStyle, children: subtree }) });
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// Icons are not a supplied value: every provider gives its subtree the same
|
|
20
|
+
// icon treatment. An icon follows its text size and color, with a constant
|
|
21
|
+
// 1.5px stroke; a `size` or `strokeWidth` on the icon still wins.
|
|
22
|
+
return _jsx(LucideProvider, { size: iconSize, strokeWidth: 1.5, nonScalingStroke: true, children: subtree });
|
|
20
23
|
}
|
|
24
|
+
// Lucide types its default size as a number, but passes it to the SVG size
|
|
25
|
+
// attributes, which take any CSS length.
|
|
26
|
+
const iconSize = "1em";
|
|
21
27
|
function AppearanceBoundary({ children, update }) {
|
|
22
28
|
const inherited = useAppearance();
|
|
23
29
|
const appearance = useMemo(() => mergeAppearance(inherited, update), [inherited, update]);
|
|
@@ -32,31 +38,13 @@ function PreferencesBoundary({ children, update }) {
|
|
|
32
38
|
return _jsx(InheritedPreferencesBoundary, { update: update, children: children });
|
|
33
39
|
}
|
|
34
40
|
function CompletePreferencesBoundary({ animations, children, theme }) {
|
|
35
|
-
const
|
|
36
|
-
const source = useMemo(() => fixedPreferencesSource(preferences), [preferences]);
|
|
41
|
+
const source = useMemo(() => fixedPreferencesSource(Object.freeze({ theme, animations })), [theme, animations]);
|
|
37
42
|
return _jsx(PreferencesContext.Provider, { value: source, children: children });
|
|
38
43
|
}
|
|
39
44
|
function InheritedPreferencesBoundary({ children, update }) {
|
|
40
45
|
const inherited = usePreferences();
|
|
41
|
-
const
|
|
42
|
-
const
|
|
46
|
+
const theme = update.theme ?? inherited.theme;
|
|
47
|
+
const animations = update.animations ?? inherited.animations;
|
|
48
|
+
const source = useMemo(() => fixedPreferencesSource(Object.freeze({ theme, animations })), [theme, animations]);
|
|
43
49
|
return _jsx(PreferencesContext.Provider, { value: source, children: children });
|
|
44
50
|
}
|
|
45
|
-
function mergeAppearance(appearance, update) {
|
|
46
|
-
return Object.freeze({
|
|
47
|
-
colors: mergeThemed(appearance.colors, update.colors),
|
|
48
|
-
spacing: update.spacing ?? appearance.spacing,
|
|
49
|
-
radius: update.radius ?? appearance.radius,
|
|
50
|
-
shadow: mergeThemed(appearance.shadow, update.shadow),
|
|
51
|
-
material: mergeThemed(appearance.material, update.material),
|
|
52
|
-
transaction: Object.freeze({ ...appearance.transaction, ...update.transaction }),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function mergeThemed(current, update) {
|
|
56
|
-
if (update === undefined)
|
|
57
|
-
return current;
|
|
58
|
-
return Object.freeze({
|
|
59
|
-
light: Object.freeze({ ...current.light, ...update.light }),
|
|
60
|
-
dark: Object.freeze({ ...current.dark, ...update.dark })
|
|
61
|
-
});
|
|
62
|
-
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
|
-
import type { Appearance } from "./appearance.js";
|
|
3
2
|
import { type ScaleLevel } from "./scale.js";
|
|
4
3
|
/** An Appearance-derived level, fully rounded shape, pixel value, or explicit CSS corner radius. */
|
|
5
4
|
export type Radius = ScaleLevel | "full" | number | (string & {});
|
|
@@ -7,5 +6,9 @@ export type Radius = ScaleLevel | "full" | number | (string & {});
|
|
|
7
6
|
export interface RadiusProps {
|
|
8
7
|
readonly radius?: Radius;
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a Radius against a base radius: a level scales the base, `full`
|
|
11
|
+
* rounds completely, and pixels and CSS pass through. Only the base is needed,
|
|
12
|
+
* so it can come from an Appearance or from anywhere else.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveRadius(value: Radius | undefined, radius: number): CSSProperties["borderRadius"];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isScaleLevel, scale } from "./scale.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a Radius against a base radius: a level scales the base, `full`
|
|
4
|
+
* rounds completely, and pixels and CSS pass through. Only the base is needed,
|
|
5
|
+
* so it can come from an Appearance or from anywhere else.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveRadius(value, radius) {
|
|
8
|
+
if (value === "full")
|
|
9
|
+
return "9999px";
|
|
10
|
+
return isScaleLevel(value) ? scale(radius, value) : value;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type ScaleLevel } from "./scale.js";
|
|
3
|
+
/** An Appearance-derived level, pixel value, or explicit CSS spacing value. */
|
|
4
|
+
export type Spacing = ScaleLevel | number | (string & {});
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a spacing value against a base spacing: a level scales the base,
|
|
7
|
+
* while pixels and CSS pass through. Only the base is needed, so it can come
|
|
8
|
+
* from an Appearance or from anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSpacing(value: ScaleLevel, spacing: number): number;
|
|
11
|
+
export declare function resolveSpacing(value: Spacing | undefined, spacing: number): CSSProperties["gap"];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Appearance, type AppearanceColors, type AppearanceMaterial, type AppearanceShadow, type Easing, type ThemedValue } from "./appearance.js";
|
|
2
|
+
import type { Preferences } from "./preferences.js";
|
|
3
|
+
/**
|
|
4
|
+
* One Appearance resolved for one set of Preferences. Every component reads
|
|
5
|
+
* this single object instead of deriving its own view of Appearance, so equal
|
|
6
|
+
* inputs share one identity across the whole tree.
|
|
7
|
+
*/
|
|
8
|
+
export interface Visual {
|
|
9
|
+
readonly appearance: Appearance;
|
|
10
|
+
readonly preferences: Preferences;
|
|
11
|
+
readonly colors: AppearanceColors;
|
|
12
|
+
readonly material: AppearanceMaterial;
|
|
13
|
+
readonly shadow: AppearanceShadow;
|
|
14
|
+
readonly spacing: number;
|
|
15
|
+
readonly radius: number;
|
|
16
|
+
/** Duration in milliseconds, already zero when animations are disabled. */
|
|
17
|
+
readonly duration: number;
|
|
18
|
+
/** CSS timing function for the Appearance transaction. */
|
|
19
|
+
readonly easing: string;
|
|
20
|
+
}
|
|
21
|
+
/** Resolves and memoizes one Visual per Appearance, Theme, and animation preference. */
|
|
22
|
+
export declare function resolveVisual(appearance: Appearance, preferences: Preferences): Visual;
|
|
23
|
+
export declare const AppearanceContext: import("react").Context<Appearance>;
|
|
24
|
+
export interface PreferencesSource {
|
|
25
|
+
readonly getSnapshot: () => Preferences;
|
|
26
|
+
readonly getServerSnapshot: () => Preferences;
|
|
27
|
+
readonly subscribe: (change: () => void) => () => void;
|
|
28
|
+
}
|
|
29
|
+
/** Returns the nearest Appearance, or React UI's default. */
|
|
30
|
+
export declare function useAppearance(): Appearance;
|
|
31
|
+
/** Returns the nearest complete Preferences, or reactively follows the browser. */
|
|
32
|
+
export declare function usePreferences(): Preferences;
|
|
33
|
+
/** Returns the shared resolved Visual for the nearest Appearance and Preferences. */
|
|
34
|
+
export declare function useVisual(): Visual;
|
|
35
|
+
/** Selects the active branch of one themed value. */
|
|
36
|
+
export declare function useThemedValue<Value>(value: ThemedValue<Value>): Value;
|
|
37
|
+
/** Reactively reads the browser's complete visual preferences. */
|
|
38
|
+
export declare function useBrowserPreferences(): Preferences;
|
|
39
|
+
/** Creates a stable source for one explicitly supplied Preferences value. */
|
|
40
|
+
export declare function fixedPreferencesSource(preferences: Preferences): PreferencesSource;
|
|
41
|
+
export declare function cssEasing(easing: Easing): string;
|
|
42
|
+
export declare const PreferencesContext: import("react").Context<PreferencesSource>;
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import { createContext, useContext,
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
2
2
|
import { defaultAppearance } from "./appearance.js";
|
|
3
|
+
const visuals = new WeakMap();
|
|
4
|
+
/** Resolves and memoizes one Visual per Appearance, Theme, and animation preference. */
|
|
5
|
+
export function resolveVisual(appearance, preferences) {
|
|
6
|
+
let resolved = visuals.get(appearance);
|
|
7
|
+
if (resolved === undefined)
|
|
8
|
+
visuals.set(appearance, resolved = new Map());
|
|
9
|
+
const key = `${preferences.theme}:${preferences.animations}`;
|
|
10
|
+
const cached = resolved.get(key);
|
|
11
|
+
if (cached !== undefined)
|
|
12
|
+
return cached;
|
|
13
|
+
const duration = preferences.animations ? appearance.transaction.duration : 0;
|
|
14
|
+
const visual = Object.freeze({
|
|
15
|
+
appearance,
|
|
16
|
+
preferences,
|
|
17
|
+
colors: appearance.colors[preferences.theme],
|
|
18
|
+
material: appearance.material[preferences.theme],
|
|
19
|
+
shadow: appearance.shadow[preferences.theme],
|
|
20
|
+
spacing: appearance.spacing,
|
|
21
|
+
radius: appearance.radius,
|
|
22
|
+
duration,
|
|
23
|
+
easing: cssEasing(appearance.transaction.easing)
|
|
24
|
+
});
|
|
25
|
+
resolved.set(key, visual);
|
|
26
|
+
return visual;
|
|
27
|
+
}
|
|
3
28
|
export const AppearanceContext = createContext(defaultAppearance);
|
|
4
|
-
/** Returns the nearest
|
|
29
|
+
/** Returns the nearest Appearance, or React UI's default. */
|
|
5
30
|
export function useAppearance() {
|
|
6
31
|
return useContext(AppearanceContext);
|
|
7
32
|
}
|
|
@@ -10,35 +35,29 @@ export function usePreferences() {
|
|
|
10
35
|
const source = useContext(PreferencesContext);
|
|
11
36
|
return useSyncExternalStore(source.subscribe, source.getSnapshot, source.getServerSnapshot);
|
|
12
37
|
}
|
|
13
|
-
/**
|
|
14
|
-
export function
|
|
15
|
-
return
|
|
38
|
+
/** Returns the shared resolved Visual for the nearest Appearance and Preferences. */
|
|
39
|
+
export function useVisual() {
|
|
40
|
+
return resolveVisual(useAppearance(), usePreferences());
|
|
16
41
|
}
|
|
17
|
-
/** Selects the active branch of one
|
|
42
|
+
/** Selects the active branch of one themed value. */
|
|
18
43
|
export function useThemedValue(value) {
|
|
19
44
|
return value[usePreferences().theme];
|
|
20
45
|
}
|
|
21
|
-
/**
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
const preferences = usePreferences();
|
|
25
|
-
return useMemo(() => ({
|
|
26
|
-
appearance,
|
|
27
|
-
preferences,
|
|
28
|
-
colors: appearance.colors[preferences.theme],
|
|
29
|
-
material: appearance.material[preferences.theme],
|
|
30
|
-
shadow: appearance.shadow[preferences.theme],
|
|
31
|
-
transaction: appearance.transaction
|
|
32
|
-
}), [appearance, preferences]);
|
|
46
|
+
/** Reactively reads the browser's complete visual preferences. */
|
|
47
|
+
export function useBrowserPreferences() {
|
|
48
|
+
return useSyncExternalStore(browserPreferencesSource.subscribe, browserPreferencesSource.getSnapshot, browserPreferencesSource.getServerSnapshot);
|
|
33
49
|
}
|
|
34
|
-
/** Creates a stable source for
|
|
50
|
+
/** Creates a stable source for one explicitly supplied Preferences value. */
|
|
35
51
|
export function fixedPreferencesSource(preferences) {
|
|
36
52
|
return {
|
|
37
53
|
getSnapshot: () => preferences,
|
|
38
54
|
getServerSnapshot: () => preferences,
|
|
39
|
-
subscribe:
|
|
55
|
+
subscribe: () => () => undefined
|
|
40
56
|
};
|
|
41
57
|
}
|
|
58
|
+
export function cssEasing(easing) {
|
|
59
|
+
return typeof easing === "string" ? easing : `cubic-bezier(${easing.join(", ")})`;
|
|
60
|
+
}
|
|
42
61
|
const defaultPreferences = Object.freeze({ theme: "light", animations: true });
|
|
43
62
|
const subscribers = new Set();
|
|
44
63
|
let darkPreference;
|
|
@@ -47,8 +66,8 @@ let browserSnapshot = defaultPreferences;
|
|
|
47
66
|
function readBrowserPreferences() {
|
|
48
67
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
49
68
|
return defaultPreferences;
|
|
50
|
-
darkPreference ??= window.matchMedia(
|
|
51
|
-
reducedMotionPreference ??= window.matchMedia(
|
|
69
|
+
darkPreference ??= window.matchMedia("(prefers-color-scheme: dark)");
|
|
70
|
+
reducedMotionPreference ??= window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
52
71
|
const theme = darkPreference.matches ? "dark" : "light";
|
|
53
72
|
const animations = !reducedMotionPreference.matches;
|
|
54
73
|
if (browserSnapshot.theme !== theme || browserSnapshot.animations !== animations) {
|
|
@@ -78,14 +97,9 @@ function publishBrowserPreferences() {
|
|
|
78
97
|
for (const subscriber of subscribers)
|
|
79
98
|
subscriber();
|
|
80
99
|
}
|
|
81
|
-
function emptySubscription() {
|
|
82
|
-
return () => undefined;
|
|
83
|
-
}
|
|
84
100
|
const browserPreferencesSource = {
|
|
85
101
|
getSnapshot: readBrowserPreferences,
|
|
86
102
|
getServerSnapshot: () => defaultPreferences,
|
|
87
103
|
subscribe: subscribeBrowserPreferences
|
|
88
104
|
};
|
|
89
105
|
export const PreferencesContext = createContext(browserPreferencesSource);
|
|
90
|
-
const darkThemeQuery = "(prefers-color-scheme: dark)";
|
|
91
|
-
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
|