@phreshos/react-ui 0.1.21 → 0.1.23
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 +16 -14
- package/dist/appearance-context.d.ts +9 -4
- package/dist/appearance-context.js +22 -18
- package/dist/appearance-provider.d.ts +5 -5
- package/dist/appearance-provider.js +7 -8
- package/dist/button.d.ts +1 -1
- package/dist/button.js +2 -1
- package/dist/color.js +10 -13
- package/dist/control.d.ts +0 -1
- package/dist/control.js +5 -5
- package/dist/main.d.ts +3 -1
- package/dist/main.js +2 -1
- package/dist/motion-style.d.ts +2 -1
- package/dist/motion-style.js +9 -10
- package/dist/scroll-area.d.ts +10 -0
- package/dist/scroll-area.js +51 -0
- package/dist/select.js +14 -14
- package/dist/slider.js +5 -4
- package/dist/toggle-indicator.js +6 -5
- package/package.json +3 -3
- package/dist/document-scrollbars.d.ts +0 -6
- package/dist/document-scrollbars.js +0 -164
package/README.md
CHANGED
|
@@ -38,27 +38,30 @@ import { Button, Surface } from "@phreshos/react-ui"
|
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Without a provider, components use Core's `defaultAppearance` and reactively
|
|
41
|
-
follow the browser
|
|
42
|
-
|
|
41
|
+
follow the browser's complete visual preferences. `AppearanceProvider` can replace
|
|
42
|
+
Appearance or Preferences for a subtree; omitted values inherit from the nearest provider.
|
|
43
43
|
React UI also exports that same canonical `defaultAppearance` value for callers
|
|
44
44
|
that need it explicitly.
|
|
45
45
|
See [Appearance](https://docs.phreshos.com/system/appearance) for the contract
|
|
46
46
|
interpreted by the provider and components.
|
|
47
47
|
|
|
48
48
|
```tsx
|
|
49
|
-
import { AppearanceProvider, Button } from "@phreshos/react-ui"
|
|
49
|
+
import { AppearanceProvider, Button, useBrowserPreferences } from "@phreshos/react-ui"
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
function Example() {
|
|
52
|
+
const browser = useBrowserPreferences()
|
|
53
|
+
|
|
54
|
+
return <AppearanceProvider preferences={{ ...browser, theme: "dark" }}>
|
|
55
|
+
<Button>Dark subtree</Button>
|
|
56
|
+
</AppearanceProvider>
|
|
57
|
+
}
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
`Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `Radio`
|
|
57
61
|
use the shared Surface implementation while retaining their native behavior. Color and material
|
|
58
62
|
are independent inputs. `color` accepts an Appearance color and resting level such
|
|
59
|
-
as `background:base` or `primary:soft`, or a direct CSS color. Surface
|
|
60
|
-
|
|
61
|
-
default required by its own behavior, such as `primary:base` for selection.
|
|
63
|
+
as `background:base` or `primary:soft`, or a direct CSS color. Surface defaults
|
|
64
|
+
to `background:base`; solid controls default to `default:base`.
|
|
62
65
|
Hover and press derive shades of that fill. The resting fill chooses the
|
|
63
66
|
higher-contrast text from Appearance's background and foreground once; interaction
|
|
64
67
|
shades keep that choice.
|
|
@@ -128,8 +131,7 @@ Native properties and the forwarded ref target the outer Surface.
|
|
|
128
131
|
|
|
129
132
|
Every input uses Appearance colors and the same five `size` levels as Button.
|
|
130
133
|
`color` selects the fill independently from material. Input, Textarea, Select,
|
|
131
|
-
and Button use `
|
|
132
|
-
`primary:base`. Invalid
|
|
134
|
+
and Button use `default:base` when it is omitted. Invalid
|
|
133
135
|
fields use danger instead. Text fields share Surface's glass edge. Interaction shades
|
|
134
136
|
derive from the base color's lightness, and text or selection marks use whichever
|
|
135
137
|
Appearance background or foreground has higher contrast against the base fill.
|
|
@@ -142,14 +144,14 @@ Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
|
|
|
142
144
|
Shared CSS transitions use `appearance.transaction` for colors and corner
|
|
143
145
|
radius. The material fill and opacity values transition on the painted layers, not on the
|
|
144
146
|
Surface host. Select menus combine a small placement-aware slide with an overlay
|
|
145
|
-
opacity fade for entry and exit, using React Aria's animation lifecycle.
|
|
147
|
+
opacity fade for entry and exit, using React Aria's animation lifecycle. Preferences with animations disabled make
|
|
146
148
|
these changes immediate without changing the Appearance value.
|
|
147
149
|
Blur, distortion, geometry, and Surface host opacity are not transitioned; gradients
|
|
148
150
|
and structurally removed effects change directly rather than adding extra
|
|
149
151
|
layers or keeping disabled effects alive.
|
|
150
152
|
|
|
151
153
|
Motion animates toggle presses, selection marks, switch travel, the Select
|
|
152
|
-
chevron, and Slider thumb feedback.
|
|
154
|
+
chevron, and Slider thumb feedback. Preferences with animations disabled remove spatial
|
|
153
155
|
feedback and make state transitions immediate. Slider values and native input
|
|
154
156
|
behavior are never delayed by visual animation.
|
|
155
157
|
|
|
@@ -203,7 +205,7 @@ import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider }
|
|
|
203
205
|
|
|
204
206
|
React Aria owns focus, keyboard, form, and selection behavior. Select's popup
|
|
205
207
|
uses Surface defaults. Toggle indicators use Motion internally and respect
|
|
206
|
-
|
|
208
|
+
animation preferences. The preview Program demonstrates each input's
|
|
207
209
|
sizes, colors, state, and interaction without overriding its visual defaults.
|
|
208
210
|
|
|
209
211
|
## Development
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Appearance, type
|
|
1
|
+
import { type Appearance, type DesktopPreferences as Preferences, type ThemedValue } from "@phreshos/core";
|
|
2
2
|
export declare const AppearanceContext: import("react").Context<Readonly<{
|
|
3
3
|
colors: ThemedValue<import("@phreshos/core").AppearanceColors>;
|
|
4
4
|
spacing: number;
|
|
@@ -9,10 +9,15 @@ export declare const AppearanceContext: import("react").Context<Readonly<{
|
|
|
9
9
|
signInWallpaper: ThemedValue<string | null>;
|
|
10
10
|
desktopWallpaper: ThemedValue<string | null>;
|
|
11
11
|
}>>;
|
|
12
|
-
export declare const
|
|
12
|
+
export declare const PreferencesContext: import("react").Context<Readonly<{
|
|
13
|
+
theme: import("@phreshos/core").Theme;
|
|
14
|
+
animations: boolean;
|
|
15
|
+
}> | null>;
|
|
13
16
|
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
14
17
|
export declare function useAppearance(): Appearance;
|
|
15
|
-
/** Returns the nearest
|
|
16
|
-
export declare function
|
|
18
|
+
/** Returns the nearest complete Preferences, or reactively follows the browser. */
|
|
19
|
+
export declare function usePreferences(): Preferences;
|
|
20
|
+
/** Reactively reads the browser's complete visual preferences. */
|
|
21
|
+
export declare function useBrowserPreferences(): Preferences;
|
|
17
22
|
/** Selects the active branch of one complete ThemedValue. */
|
|
18
23
|
export declare function useThemedValue<Value>(value: ThemedValue<Value>): Value;
|
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
1
|
+
import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
|
|
2
2
|
import { defaultAppearance } from "@phreshos/core";
|
|
3
3
|
export const AppearanceContext = createContext(defaultAppearance);
|
|
4
|
-
export const
|
|
4
|
+
export const PreferencesContext = createContext(null);
|
|
5
5
|
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
6
6
|
export function useAppearance() {
|
|
7
7
|
return useContext(AppearanceContext);
|
|
8
8
|
}
|
|
9
|
-
/** Returns the nearest
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
return
|
|
9
|
+
/** Returns the nearest complete Preferences, or reactively follows the browser. */
|
|
10
|
+
export function usePreferences() {
|
|
11
|
+
const preferences = useContext(PreferencesContext);
|
|
12
|
+
const browserPreferences = useBrowserPreferences();
|
|
13
|
+
return preferences ?? browserPreferences;
|
|
14
|
+
}
|
|
15
|
+
/** Reactively reads the browser's complete visual preferences. */
|
|
16
|
+
export function useBrowserPreferences() {
|
|
17
|
+
const theme = useMediaPreference(darkThemeQuery) ? "dark" : "light";
|
|
18
|
+
const animations = !useMediaPreference(reducedMotionQuery);
|
|
19
|
+
return useMemo(() => ({ theme, animations }), [animations, theme]);
|
|
14
20
|
}
|
|
15
21
|
/** Selects the active branch of one complete ThemedValue. */
|
|
16
22
|
export function useThemedValue(value) {
|
|
17
|
-
return value[
|
|
23
|
+
return value[usePreferences().theme];
|
|
18
24
|
}
|
|
19
|
-
function
|
|
20
|
-
return useSyncExternalStore(
|
|
25
|
+
function useMediaPreference(query) {
|
|
26
|
+
return useSyncExternalStore(change => subscribeMediaPreference(query, change), () => browserMediaPreference(query), () => false);
|
|
21
27
|
}
|
|
22
|
-
function
|
|
28
|
+
function subscribeMediaPreference(query, change) {
|
|
23
29
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
24
30
|
return () => undefined;
|
|
25
|
-
const preference = window.matchMedia(
|
|
31
|
+
const preference = window.matchMedia(query);
|
|
26
32
|
preference.addEventListener("change", change);
|
|
27
33
|
return () => preference.removeEventListener("change", change);
|
|
28
34
|
}
|
|
29
|
-
function
|
|
30
|
-
return typeof window !== "undefined"
|
|
35
|
+
function browserMediaPreference(query) {
|
|
36
|
+
return Boolean(typeof window !== "undefined"
|
|
31
37
|
&& typeof window.matchMedia === "function"
|
|
32
|
-
&& window.matchMedia(
|
|
33
|
-
? "dark"
|
|
34
|
-
: "light";
|
|
38
|
+
&& window.matchMedia(query).matches);
|
|
35
39
|
}
|
|
36
|
-
function serverTheme() { return "light"; }
|
|
37
40
|
const darkThemeQuery = "(prefers-color-scheme: dark)";
|
|
41
|
+
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { type Appearance, type
|
|
3
|
-
export { useAppearance,
|
|
4
|
-
/** Optionally overrides
|
|
5
|
-
export declare function AppearanceProvider({ appearance, children,
|
|
2
|
+
import { type Appearance, type DesktopPreferences as Preferences } from "@phreshos/core";
|
|
3
|
+
export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
|
|
4
|
+
/** Optionally overrides Appearance and complete visual Preferences for a React subtree. */
|
|
5
|
+
export declare function AppearanceProvider({ appearance, children, preferences }: AppearanceProviderProps): import("react").JSX.Element;
|
|
6
6
|
export interface AppearanceProviderProps {
|
|
7
7
|
readonly appearance?: Appearance;
|
|
8
8
|
readonly children: ReactNode;
|
|
9
|
-
readonly
|
|
9
|
+
readonly preferences?: Preferences;
|
|
10
10
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import {} from "@phreshos/core";
|
|
3
|
-
import DocumentScrollbars from "./document-scrollbars.js";
|
|
4
3
|
import MotionStyle from "./motion-style.js";
|
|
5
|
-
import { AppearanceContext,
|
|
6
|
-
export { useAppearance,
|
|
7
|
-
/** Optionally overrides
|
|
8
|
-
export function AppearanceProvider({ appearance, children,
|
|
4
|
+
import { AppearanceContext, PreferencesContext, useAppearance, usePreferences } from "./appearance-context.js";
|
|
5
|
+
export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
|
|
6
|
+
/** Optionally overrides Appearance and complete visual Preferences for a React subtree. */
|
|
7
|
+
export function AppearanceProvider({ appearance, children, preferences }) {
|
|
9
8
|
const inheritedAppearance = useAppearance();
|
|
10
|
-
const
|
|
9
|
+
const inheritedPreferences = usePreferences();
|
|
11
10
|
const resolvedAppearance = appearance ?? inheritedAppearance;
|
|
12
|
-
const
|
|
13
|
-
return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(
|
|
11
|
+
const resolvedPreferences = preferences ?? inheritedPreferences;
|
|
12
|
+
return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(PreferencesContext.Provider, { value: resolvedPreferences, children: [_jsx(MotionStyle, {}), children] }) });
|
|
14
13
|
}
|
package/dist/button.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { RadiusProps } from "./radius.js";
|
|
|
5
5
|
import { type ControlColor } from "./control.js";
|
|
6
6
|
import type { MaterialOverrides } from "./material-options.js";
|
|
7
7
|
type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
|
|
8
|
-
/** A semantic color from Appearance; omission uses
|
|
8
|
+
/** A semantic color from Appearance; omission uses `default:base`. */
|
|
9
9
|
export type ButtonColor = ControlColor;
|
|
10
10
|
/** Properties accepted by the shared interactive button. */
|
|
11
11
|
export interface ButtonProps extends NativeButtonProps, RadiusProps, MaterialOverrides {
|
package/dist/button.js
CHANGED
|
@@ -31,6 +31,7 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
|
|
|
31
31
|
gridAutoFlow: "column",
|
|
32
32
|
gridAutoColumns: "max-content",
|
|
33
33
|
placeItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
34
35
|
flexShrink: 0,
|
|
35
36
|
minWidth: 0,
|
|
36
37
|
height,
|
|
@@ -56,6 +57,6 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
function buttonPaint(theme, interactive, hovered, pressed) {
|
|
59
|
-
const paints = theme.
|
|
60
|
+
const paints = theme.paints.palette;
|
|
60
61
|
return interactive && pressed ? paints.pressed : interactive && hovered ? paints.hover : paints.rest;
|
|
61
62
|
}
|
package/dist/color.js
CHANGED
|
@@ -43,16 +43,16 @@ export function useColor(value) {
|
|
|
43
43
|
/** Resolves a semantic Appearance color or preserves a direct CSS color. */
|
|
44
44
|
export function useResolveColor(value = defaultColor) {
|
|
45
45
|
const appearance = useAppearance();
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
return semantic ? color(source)[semantic.level] : value;
|
|
46
|
+
const colors = useThemedValue(appearance.colors);
|
|
47
|
+
const semantic = parseSemanticColor(value, colors);
|
|
48
|
+
return semantic ? color(semantic.source)[semantic.level] : value;
|
|
49
49
|
}
|
|
50
50
|
/** Resolves a color to concrete opaque paint for solid interactive states. */
|
|
51
51
|
export function useResolveSolidColor(value) {
|
|
52
52
|
const appearance = useAppearance();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
return semantic ? resolveColorLevel(source, semantic.level) : opaqueColor(value);
|
|
53
|
+
const colors = useThemedValue(appearance.colors);
|
|
54
|
+
const semantic = parseSemanticColor(value, colors);
|
|
55
|
+
return semantic ? resolveColorLevel(semantic.source, semantic.level) : opaqueColor(value);
|
|
56
56
|
}
|
|
57
57
|
/** Applies material opacity without restricting the source CSS color syntax. */
|
|
58
58
|
export function colorOpacity(value, opacity) {
|
|
@@ -99,17 +99,14 @@ export function solidColors(base, background, foreground) {
|
|
|
99
99
|
pressed: paint(colorShade(fill, 0.085))
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
function parseSemanticColor(value) {
|
|
102
|
+
function parseSemanticColor(value, colors) {
|
|
103
103
|
const separator = value.indexOf(":");
|
|
104
104
|
if (separator < 0 || value.indexOf(":", separator + 1) >= 0)
|
|
105
105
|
return null;
|
|
106
106
|
const name = value.slice(0, separator);
|
|
107
107
|
const level = value.slice(separator + 1);
|
|
108
|
-
|
|
108
|
+
const source = Reflect.get(colors, name);
|
|
109
|
+
if (typeof source !== "string" || !isColorLevel(level))
|
|
109
110
|
return null;
|
|
110
|
-
return {
|
|
111
|
-
}
|
|
112
|
-
function isAppearanceColor(value) {
|
|
113
|
-
return value === "background" || value === "foreground" || value === "primary" || value === "secondary"
|
|
114
|
-
|| value === "success" || value === "warning" || value === "danger" || value === "info";
|
|
111
|
+
return { source, level };
|
|
115
112
|
}
|
package/dist/control.d.ts
CHANGED
package/dist/control.js
CHANGED
|
@@ -21,20 +21,20 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
|
21
21
|
const colors = useThemedValue(appearance.colors);
|
|
22
22
|
const foreground = colors.foreground;
|
|
23
23
|
const background = colors.background;
|
|
24
|
-
const tint = useResolveSolidColor(color ?? "
|
|
24
|
+
const tint = useResolveSolidColor(color ?? "default:base");
|
|
25
|
+
const neutral = useResolveSolidColor("default:base");
|
|
25
26
|
const danger = useResolveSolidColor("danger:base");
|
|
26
27
|
const paints = useMemo(() => ({
|
|
27
28
|
palette: solidColors(tint, background, foreground),
|
|
28
|
-
neutral: solidColors(
|
|
29
|
+
neutral: solidColors(neutral, background, foreground),
|
|
29
30
|
danger: solidColors(danger, background, foreground)
|
|
30
|
-
}), [tint, background, foreground, danger]);
|
|
31
|
+
}), [tint, neutral, background, foreground, danger]);
|
|
31
32
|
return {
|
|
32
33
|
transition,
|
|
33
34
|
spacing,
|
|
34
35
|
foreground,
|
|
35
36
|
background,
|
|
36
37
|
tint,
|
|
37
|
-
colored: color !== undefined,
|
|
38
38
|
danger,
|
|
39
39
|
paints,
|
|
40
40
|
radius: resolveRadius(radius, appearance),
|
|
@@ -58,7 +58,7 @@ export function fieldStyle(theme, disabled = false, style) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
export function controlPaint(theme, focused, invalid, hovered = false) {
|
|
61
|
-
const paints = invalid ? theme.paints.danger : theme.
|
|
61
|
+
const paints = invalid ? theme.paints.danger : theme.paints.palette;
|
|
62
62
|
return focused ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
63
63
|
}
|
|
64
64
|
export function controlStyle(theme, focused, invalid, hovered = false, focusVisible = focused) {
|
package/dist/main.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
7
|
export { defaultAppearance } from "@phreshos/core";
|
|
8
|
-
export { AppearanceProvider, useAppearance,
|
|
8
|
+
export { AppearanceProvider, useAppearance, useBrowserPreferences, usePreferences, useThemedValue, type AppearanceProviderProps } from "./appearance-provider.js";
|
|
9
|
+
export type { DesktopPreferences as Preferences } from "@phreshos/core";
|
|
9
10
|
export { Flex, type FlexProps } from "./flex.js";
|
|
10
11
|
export { Grid, type GridProps } from "./grid.js";
|
|
11
12
|
export { Surface, type MaterialOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
|
|
@@ -19,6 +20,7 @@ export { Radio, RadioGroup, type RadioProps, type RadioGroupProps } from "./radi
|
|
|
19
20
|
export { Switch, type SwitchProps } from "./switch.js";
|
|
20
21
|
export { Select, type SelectProps, type SelectOption } from "./select.js";
|
|
21
22
|
export { Slider, type SliderProps } from "./slider.js";
|
|
23
|
+
export { ScrollArea, type ScrollAreaAxis, type ScrollAreaProps } from "./scroll-area.js";
|
|
22
24
|
export type { ControlColor } from "./control.js";
|
|
23
25
|
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
24
26
|
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
7
|
export { defaultAppearance } from "@phreshos/core";
|
|
8
|
-
export { AppearanceProvider, useAppearance,
|
|
8
|
+
export { AppearanceProvider, useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-provider.js";
|
|
9
9
|
export { Flex } from "./flex.js";
|
|
10
10
|
export { Grid } from "./grid.js";
|
|
11
11
|
export { Surface } from "./surface.js";
|
|
@@ -19,6 +19,7 @@ export { Radio, RadioGroup } from "./radio.js";
|
|
|
19
19
|
export { Switch } from "./switch.js";
|
|
20
20
|
export { Select } from "./select.js";
|
|
21
21
|
export { Slider } from "./slider.js";
|
|
22
|
+
export { ScrollArea } from "./scroll-area.js";
|
|
22
23
|
export { resolveRadius } from "./radius.js";
|
|
23
24
|
export { resolveSpacing } from "./spacing.js";
|
|
24
25
|
export { useColor } from "./color.js";
|
package/dist/motion-style.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ export declare function useVisualTransition(): CSSProperties;
|
|
|
5
5
|
/** CSS timing for SVG paint owned by a Material. */
|
|
6
6
|
export declare function usePaintTransition(): CSSProperties;
|
|
7
7
|
/** Motion timing for interactive geometry owned by React UI. */
|
|
8
|
-
export declare function useControlTransition(
|
|
8
|
+
export declare function useControlTransition(): Transition;
|
|
9
9
|
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
10
10
|
export declare function useOverlayTransition(): CSSProperties;
|
|
11
11
|
export declare const overlayMotionClass = "phreshos-ui-overlay";
|
|
12
|
+
export declare function useTransitionTiming(): CSSProperties;
|
|
12
13
|
/** React hoists and deduplicates this stylesheet, including portals. */
|
|
13
14
|
export default function MotionStyle(): import("react").JSX.Element;
|
package/dist/motion-style.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { defaultAppearance } from "@phreshos/core";
|
|
3
|
-
import { useAppearance } from "./appearance-context.js";
|
|
3
|
+
import { useAppearance, usePreferences } from "./appearance-context.js";
|
|
4
4
|
const visualProperties = "background-color, color, border-color, border-radius";
|
|
5
5
|
const paintProperties = "fill, stroke, opacity";
|
|
6
6
|
/** CSS timing for values painted directly by React UI. */
|
|
@@ -12,27 +12,30 @@ export function usePaintTransition() {
|
|
|
12
12
|
return { ...useTransitionTiming(), transitionProperty: paintProperties };
|
|
13
13
|
}
|
|
14
14
|
/** Motion timing for interactive geometry owned by React UI. */
|
|
15
|
-
export function useControlTransition(
|
|
15
|
+
export function useControlTransition() {
|
|
16
16
|
const { duration, easing } = useAppearance().transaction;
|
|
17
|
+
const { animations } = usePreferences();
|
|
17
18
|
return {
|
|
18
19
|
type: "tween",
|
|
19
|
-
duration:
|
|
20
|
+
duration: animations ? duration / 1_000 : 0,
|
|
20
21
|
ease: motionEasing(easing)
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
24
25
|
export function useOverlayTransition() {
|
|
25
26
|
const { duration, easing } = useAppearance().transaction;
|
|
27
|
+
const { animations } = usePreferences();
|
|
26
28
|
return {
|
|
27
|
-
"--phreshos-ui-motion-duration": `${duration}ms`,
|
|
29
|
+
"--phreshos-ui-motion-duration": `${animations ? duration : 0}ms`,
|
|
28
30
|
"--phreshos-ui-motion-easing": cssEasing(easing)
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
export const overlayMotionClass = "phreshos-ui-overlay";
|
|
32
|
-
function useTransitionTiming() {
|
|
34
|
+
export function useTransitionTiming() {
|
|
33
35
|
const { duration, easing } = useAppearance().transaction;
|
|
36
|
+
const { animations } = usePreferences();
|
|
34
37
|
return {
|
|
35
|
-
transitionDuration: `${duration}ms`,
|
|
38
|
+
transitionDuration: `${animations ? duration : 0}ms`,
|
|
36
39
|
transitionTimingFunction: cssEasing(easing)
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -73,10 +76,6 @@ const stylesheet = `
|
|
|
73
76
|
from { opacity: 0; translate: var(--phreshos-ui-overlay-x) var(--phreshos-ui-overlay-y); }
|
|
74
77
|
to { opacity: 1; translate: 0 0; }
|
|
75
78
|
}
|
|
76
|
-
@media (prefers-reduced-motion: reduce) {
|
|
77
|
-
:root { --phreshos-ui-motion-duration: 0ms; }
|
|
78
|
-
.phreshos-ui-overlay[data-entering], .phreshos-ui-overlay[data-exiting] { animation: none; }
|
|
79
|
-
}
|
|
80
79
|
`;
|
|
81
80
|
/** React hoists and deduplicates this stylesheet, including portals. */
|
|
82
81
|
export default function MotionStyle() {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, Ref, ReactNode, UIEventHandler } from "react";
|
|
2
|
+
export type ScrollAreaAxis = "vertical" | "horizontal" | "both";
|
|
3
|
+
export interface ScrollAreaProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onScroll"> {
|
|
4
|
+
readonly axis?: ScrollAreaAxis;
|
|
5
|
+
readonly children?: ReactNode;
|
|
6
|
+
readonly onScroll?: UIEventHandler<HTMLDivElement>;
|
|
7
|
+
readonly viewportRef?: Ref<HTMLDivElement>;
|
|
8
|
+
}
|
|
9
|
+
/** A native scrolling viewport with locally owned React UI scrollbars. */
|
|
10
|
+
export declare const ScrollArea: import("react").ForwardRefExoticComponent<ScrollAreaProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
|
|
3
|
+
import { forwardRef, useState } from "react";
|
|
4
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
5
|
+
import { colorOpacity } from "./color.js";
|
|
6
|
+
import { useTransitionTiming } from "./motion-style.js";
|
|
7
|
+
/** A native scrolling viewport with locally owned React UI scrollbars. */
|
|
8
|
+
export const ScrollArea = forwardRef(function ScrollArea({ axis = "vertical", children, onScroll, style, viewportRef, ...properties }, ref) {
|
|
9
|
+
const appearance = useAppearance();
|
|
10
|
+
const foreground = useThemedValue(appearance.colors).foreground;
|
|
11
|
+
const radius = Math.min(appearance.radius, 8);
|
|
12
|
+
const vertical = axis === "vertical" || axis === "both";
|
|
13
|
+
const horizontal = axis === "horizontal" || axis === "both";
|
|
14
|
+
return _jsxs(BaseScrollArea.Root, { ...properties, "data-phreshos-scroll-area": "", ref: ref, style: {
|
|
15
|
+
...style,
|
|
16
|
+
position: style?.position ?? "relative",
|
|
17
|
+
minWidth: style?.minWidth ?? 0,
|
|
18
|
+
minHeight: style?.minHeight ?? 0,
|
|
19
|
+
overflow: "hidden"
|
|
20
|
+
}, children: [_jsx(BaseScrollArea.Viewport, { "data-phreshos-scroll-area-viewport": "", ref: viewportRef, onScroll: onScroll, style: {
|
|
21
|
+
width: "100%",
|
|
22
|
+
height: "100%",
|
|
23
|
+
overflowX: horizontal ? "scroll" : "hidden",
|
|
24
|
+
overflowY: vertical ? "scroll" : "hidden"
|
|
25
|
+
}, children: _jsx(BaseScrollArea.Content, { "data-phreshos-scroll-area-content": "", style: vertical && !horizontal ? { minWidth: "100%", width: "100%" } : undefined, children: children }) }), vertical && _jsx(Scrollbar, { orientation: "vertical", foreground: foreground, radius: radius }), horizontal && _jsx(Scrollbar, { orientation: "horizontal", foreground: foreground, radius: radius }), vertical && horizontal && _jsx(BaseScrollArea.Corner, { style: { background: "transparent" } })] });
|
|
26
|
+
});
|
|
27
|
+
function Scrollbar({ orientation, foreground, radius }) {
|
|
28
|
+
const [hovered, setHovered] = useState(false);
|
|
29
|
+
const transition = useTransitionTiming();
|
|
30
|
+
const vertical = orientation === "vertical";
|
|
31
|
+
return _jsx(BaseScrollArea.Scrollbar, { "data-phreshos-scroll-area-scrollbar": "", orientation: orientation, keepMounted: true, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), style: state => {
|
|
32
|
+
const present = vertical ? state.hasOverflowY : state.hasOverflowX;
|
|
33
|
+
return {
|
|
34
|
+
boxSizing: "border-box",
|
|
35
|
+
width: vertical ? 16 : undefined,
|
|
36
|
+
height: vertical ? undefined : 16,
|
|
37
|
+
padding: 5,
|
|
38
|
+
opacity: present && (state.hovering || state.scrolling) ? 1 : 0,
|
|
39
|
+
pointerEvents: present && (state.hovering || state.scrolling) ? "auto" : "none",
|
|
40
|
+
transitionDuration: transition.transitionDuration,
|
|
41
|
+
transitionTimingFunction: transition.transitionTimingFunction,
|
|
42
|
+
transitionProperty: "opacity"
|
|
43
|
+
};
|
|
44
|
+
}, children: _jsx(BaseScrollArea.Thumb, { "data-phreshos-scroll-area-thumb": "", style: {
|
|
45
|
+
borderRadius: radius,
|
|
46
|
+
backgroundColor: colorOpacity(foreground, hovered ? 0.2 : 0.1),
|
|
47
|
+
transitionDuration: transition.transitionDuration,
|
|
48
|
+
transitionTimingFunction: transition.transitionTimingFunction,
|
|
49
|
+
transitionProperty: "background-color"
|
|
50
|
+
} }) });
|
|
51
|
+
}
|
package/dist/select.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
|
-
import { motion
|
|
3
|
+
import { motion } from "motion/react";
|
|
4
4
|
import { Select as AriaSelect, Button, SelectValue, Popover, ListBox, ListBoxItem } from "react-aria-components";
|
|
5
5
|
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
6
6
|
import { SurfaceButton } from "./control-surface.js";
|
|
7
7
|
import { Surface } from "./surface.js";
|
|
8
|
+
import { ScrollArea } from "./scroll-area.js";
|
|
8
9
|
import { overlayMotionClass, useControlTransition, useOverlayTransition } from "./motion-style.js";
|
|
9
10
|
/** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
|
|
10
11
|
export const Select = forwardRef(function Select({ label, description, errorMessage, disabled, required, invalid, options, value, defaultValue, onChange, size, color, radius, style, material, ...properties }, ref) {
|
|
11
12
|
const theme = useControlTheme({ size, color, radius });
|
|
12
|
-
const
|
|
13
|
-
const transition = useControlTransition(Boolean(reduced));
|
|
13
|
+
const transition = useControlTransition();
|
|
14
14
|
const overlayTransition = useOverlayTransition();
|
|
15
15
|
return _jsx(AriaSelect, { ...properties, ref: ref, value: value, defaultValue: defaultValue, onChange: key => onChange?.(key == null ? null : String(key)), disabledKeys: options.filter(option => option.disabled).map(option => option.value), isDisabled: disabled, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: state => _jsxs(_Fragment, { children: [_jsx(FieldLabel, { label: label }), _jsxs(Button, { render: (native, button) => _jsx(SurfaceButton, { native: native, material: material, paint: controlPaint(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered) }), style: button => ({
|
|
16
16
|
...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
|
|
17
17
|
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
18
18
|
cursor: disabled ? "not-allowed" : "pointer", textAlign: "start"
|
|
19
|
-
}), children: [_jsx(SelectValue, { style: ({ isPlaceholder }) => ({ opacity: isPlaceholder ? 0.6 : 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }) }), _jsx(motion.svg, { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", initial: false, animate: { rotate: state.isOpen ? 180 : 0 }, transition: transition, style: { flexShrink: 0 }, children: _jsx("path", { d: "m2 4 4 4 4-4" }) })] }), _jsx(Popover, { className: overlayMotionClass, placement: "bottom start", offset: theme.gap, maxHeight: 280 + theme.gap * 2, style: { ...overlayTransition, width: "var(--trigger-width)", maxWidth: "calc(100vw - 16px)" }, children: _jsx(Surface, { style: { padding: theme.gap, display: "flex", flexDirection: "column", maxHeight: "inherit", boxSizing: "border-box" }, children: _jsx(ListBox, { items: options, style: { display: "grid", gap: theme.gap,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
}), children: [_jsx(SelectValue, { style: ({ isPlaceholder }) => ({ opacity: isPlaceholder ? 0.6 : 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }) }), _jsx(motion.svg, { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", initial: false, animate: { rotate: state.isOpen ? 180 : 0 }, transition: transition, style: { flexShrink: 0 }, children: _jsx("path", { d: "m2 4 4 4 4-4" }) })] }), _jsx(Popover, { className: overlayMotionClass, placement: "bottom start", offset: theme.gap, maxHeight: 280 + theme.gap * 2, style: { ...overlayTransition, width: "var(--trigger-width)", maxWidth: "calc(100vw - 16px)" }, children: _jsx(Surface, { style: { padding: theme.gap, display: "flex", flexDirection: "column", maxHeight: "inherit", boxSizing: "border-box" }, children: _jsx(ScrollArea, { style: { flex: "1 1 auto", minHeight: 0 }, children: _jsx(ListBox, { items: options, style: { display: "grid", gap: theme.gap, outline: "none", fontSize: theme.fontSize, color: theme.foreground }, children: option => _jsx(ListBoxItem, { id: option.value, textValue: option.label, style: item => ({
|
|
20
|
+
...theme.transition,
|
|
21
|
+
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
22
|
+
minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
|
|
23
|
+
borderRadius: theme.radius,
|
|
24
|
+
outline: item.isFocused && !item.isSelected ? `2px solid ${theme.foreground}` : "none", outlineOffset: -2,
|
|
25
|
+
cursor: item.isDisabled ? "not-allowed" : "pointer",
|
|
26
|
+
opacity: item.isDisabled ? 0.46 : 1,
|
|
27
|
+
...(item.isSelected ? item.isFocused ? theme.paints.palette.hover : theme.paints.palette.rest
|
|
28
|
+
: { background: "transparent", color: theme.foreground })
|
|
29
|
+
}), children: item => _jsxs(_Fragment, { children: [option.label, _jsx("span", { "aria-hidden": "true", children: item.isSelected ? "✓" : null })] }) }) }) }) }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] }) });
|
|
30
30
|
});
|
package/dist/slider.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useId } from "react";
|
|
3
|
-
import { motion
|
|
3
|
+
import { motion } from "motion/react";
|
|
4
4
|
import { Slider as AriaSlider, SliderTrack, SliderThumb, SliderFill, SliderOutput } from "react-aria-components";
|
|
5
5
|
import { FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
6
6
|
import { useControlTransition } from "./motion-style.js";
|
|
7
|
+
import { usePreferences } from "./appearance-provider.js";
|
|
7
8
|
/** One numeric value with pointer, touch, keyboard, and form support. */
|
|
8
9
|
export const Slider = forwardRef(function Slider({ label, description, name, disabled, size, color, style, orientation = "horizontal", ...properties }, ref) {
|
|
9
10
|
const theme = useControlTheme({ size, color });
|
|
10
11
|
const descriptionId = useId();
|
|
11
12
|
const vertical = orientation === "vertical";
|
|
12
13
|
const diameter = theme.fontSize + 6;
|
|
13
|
-
const
|
|
14
|
-
const transition = useControlTransition(
|
|
14
|
+
const { animations } = usePreferences();
|
|
15
|
+
const transition = useControlTransition();
|
|
15
16
|
const rail = Math.max(4, Math.round(diameter / 3));
|
|
16
17
|
return _jsxs(AriaSlider, { ...properties, ref: ref, isDisabled: disabled, orientation: orientation, style: fieldStyle(theme, disabled, style), "aria-describedby": [properties["aria-describedby"], description != null ? descriptionId : null].filter(Boolean).join(" ") || undefined, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: theme.gap }, children: [_jsx(FieldLabel, { label: label }), _jsx(SliderOutput, { style: { fontVariantNumeric: "tabular-nums", opacity: 0.7 } })] }), _jsxs(SliderTrack, { style: {
|
|
17
18
|
position: "relative", touchAction: "none",
|
|
@@ -30,7 +31,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, dis
|
|
|
30
31
|
...(vertical ? { left: "50%" } : { top: "50%" }),
|
|
31
32
|
outline: state.isFocusVisible ? `2px solid ${theme.foreground}` : "none", outlineOffset: 2,
|
|
32
33
|
cursor: state.isDisabled ? "not-allowed" : state.isDragging ? "grabbing" : "grab"
|
|
33
|
-
}), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale:
|
|
34
|
+
}), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: !animations || state.isDisabled ? 1 : state.isDragging ? 0.92 : state.isHovered ? 1.08 : 1 }, transition: transition, style: { ...theme.transition, position: "absolute", inset: 0, borderRadius: "inherit", pointerEvents: "none",
|
|
34
35
|
background: (state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).color,
|
|
35
36
|
border: `3px solid ${(state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).background}`,
|
|
36
37
|
boxSizing: "border-box" } }) })] }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
|
package/dist/toggle-indicator.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { motion
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
3
|
import { useLocale } from "react-aria-components";
|
|
4
4
|
import { useControlTransition } from "./motion-style.js";
|
|
5
|
+
import { usePreferences } from "./appearance-provider.js";
|
|
5
6
|
import { Surface } from "./surface.js";
|
|
6
7
|
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
7
8
|
export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
|
|
8
|
-
const
|
|
9
|
-
const transition = useControlTransition(
|
|
9
|
+
const { animations } = usePreferences();
|
|
10
|
+
const transition = useControlTransition();
|
|
10
11
|
const { direction } = useLocale();
|
|
11
12
|
const diameter = theme.fontSize + 6;
|
|
12
13
|
const active = selected || indeterminate;
|
|
@@ -14,9 +15,9 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
|
|
|
14
15
|
const paint = pressed ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
15
16
|
const radius = kind === "checkbox" ? Math.min(diameter / 4, typeof theme.radius === "number" ? theme.radius : diameter / 4) : diameter;
|
|
16
17
|
const switching = kind === "switch";
|
|
17
|
-
const thumbWidth = diameter - 6 + (pressed &&
|
|
18
|
+
const thumbWidth = diameter - 6 + (pressed && animations ? 3 : 0);
|
|
18
19
|
const travel = (diameter * 2 - thumbWidth) / 2 - 3;
|
|
19
|
-
return _jsx(Surface, { as: motion.span, material: options, color: paint.background, "aria-hidden": "true", initial: false, animate: { scale:
|
|
20
|
+
return _jsx(Surface, { as: motion.span, material: options, color: paint.background, "aria-hidden": "true", initial: false, animate: { scale: !animations || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: transition, style: {
|
|
20
21
|
color: paint.color,
|
|
21
22
|
...theme.transition,
|
|
22
23
|
display: "inline-grid",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"react-dom": "^19.2.0"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@
|
|
58
|
+
"@base-ui/react": "^1.7.0",
|
|
59
|
+
"@phreshos/core": "^0.1.49",
|
|
59
60
|
"colorjs.io": "^0.7.1",
|
|
60
61
|
"motion": "^13.2.0",
|
|
61
62
|
"react-aria-components": "^1.20.0"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
|
-
"@base-ui/react": "^1.7.0",
|
|
65
65
|
"@testing-library/dom": "^10.4.1",
|
|
66
66
|
"@testing-library/react": "^16.3.0",
|
|
67
67
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { Appearance, Theme } from "@phreshos/core";
|
|
2
|
-
/** Applies one Appearance to the complete owning document without rendering. */
|
|
3
|
-
export default function DocumentScrollbars({ appearance, theme }: Readonly<{
|
|
4
|
-
appearance: Appearance;
|
|
5
|
-
theme: Theme;
|
|
6
|
-
}>): null;
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { useInsertionEffect, useRef } from "react";
|
|
2
|
-
import { colorOpacity } from "./color.js";
|
|
3
|
-
const properties = {
|
|
4
|
-
thumb: "--phreshos-scrollbar-thumb",
|
|
5
|
-
thumbHover: "--phreshos-scrollbar-thumb-hover",
|
|
6
|
-
size: "--phreshos-scrollbar-size",
|
|
7
|
-
padding: "--phreshos-scrollbar-padding",
|
|
8
|
-
radius: "--phreshos-scrollbar-radius"
|
|
9
|
-
};
|
|
10
|
-
const stylesheet = `
|
|
11
|
-
@supports not selector(::-webkit-scrollbar) {
|
|
12
|
-
* {
|
|
13
|
-
scrollbar-color: var(${properties.thumb}) transparent;
|
|
14
|
-
scrollbar-width: thin;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@media (hover: hover) and (pointer: fine) {
|
|
18
|
-
* {
|
|
19
|
-
scrollbar-color: transparent transparent;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
*:hover {
|
|
23
|
-
scrollbar-color: var(${properties.thumb}) transparent;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@media (forced-colors: active) {
|
|
28
|
-
*,
|
|
29
|
-
*:hover {
|
|
30
|
-
scrollbar-color: auto;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@supports selector(::-webkit-scrollbar) {
|
|
36
|
-
*::-webkit-scrollbar {
|
|
37
|
-
width: var(${properties.size});
|
|
38
|
-
height: var(${properties.size});
|
|
39
|
-
background: transparent;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
*::-webkit-scrollbar-track,
|
|
43
|
-
*::-webkit-scrollbar-corner {
|
|
44
|
-
background: transparent;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
*::-webkit-scrollbar-button {
|
|
48
|
-
display: none;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
*::-webkit-scrollbar-thumb {
|
|
52
|
-
border: var(${properties.padding}) solid transparent;
|
|
53
|
-
border-radius: var(${properties.radius});
|
|
54
|
-
background-color: var(${properties.thumb});
|
|
55
|
-
background-clip: padding-box;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@media (hover: hover) and (pointer: fine) {
|
|
59
|
-
*:hover {
|
|
60
|
-
--phreshos-scrollbar-repaint: ;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
*::-webkit-scrollbar-thumb {
|
|
64
|
-
background-color: transparent;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
*:hover::-webkit-scrollbar-thumb {
|
|
68
|
-
background-color: var(${properties.thumb});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
*::-webkit-scrollbar-thumb:hover {
|
|
72
|
-
background-color: var(${properties.thumbHover});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@media (forced-colors: active) {
|
|
77
|
-
*::-webkit-scrollbar-thumb,
|
|
78
|
-
*:hover::-webkit-scrollbar-thumb,
|
|
79
|
-
*::-webkit-scrollbar-thumb:hover {
|
|
80
|
-
background-color: ButtonText;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
`;
|
|
85
|
-
const documents = new WeakMap();
|
|
86
|
-
/** Applies one Appearance to the complete owning document without rendering. */
|
|
87
|
-
export default function DocumentScrollbars({ appearance, theme }) {
|
|
88
|
-
const identity = useRef(Symbol("AppearanceProvider")).current;
|
|
89
|
-
const foreground = appearance.colors[theme].foreground;
|
|
90
|
-
const padding = 5;
|
|
91
|
-
const thumbSize = 6;
|
|
92
|
-
const size = thumbSize + padding * 2;
|
|
93
|
-
const values = {
|
|
94
|
-
thumb: colorOpacity(foreground, 0.1),
|
|
95
|
-
thumbHover: colorOpacity(foreground, 0.2),
|
|
96
|
-
size: `${size}px`,
|
|
97
|
-
padding: `${padding}px`,
|
|
98
|
-
radius: `${Math.min(appearance.radius, padding + thumbSize / 2)}px`
|
|
99
|
-
};
|
|
100
|
-
useInsertionEffect(function () {
|
|
101
|
-
if (typeof document === "undefined")
|
|
102
|
-
return;
|
|
103
|
-
return register(document, identity, values);
|
|
104
|
-
}, []);
|
|
105
|
-
useInsertionEffect(function () {
|
|
106
|
-
if (typeof document !== "undefined")
|
|
107
|
-
update(document, identity, values);
|
|
108
|
-
}, [values.thumb, values.thumbHover, values.size, values.padding, values.radius]);
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
function register(owner, identity, values) {
|
|
112
|
-
const state = documents.get(owner) ?? create(owner);
|
|
113
|
-
state.providers.set(identity, values);
|
|
114
|
-
apply(state);
|
|
115
|
-
return function () {
|
|
116
|
-
state.providers.delete(identity);
|
|
117
|
-
if (state.providers.size) {
|
|
118
|
-
apply(state);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
state.style.remove();
|
|
122
|
-
restore(state);
|
|
123
|
-
documents.delete(owner);
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function update(owner, identity, values) {
|
|
127
|
-
const state = documents.get(owner);
|
|
128
|
-
if (!state?.providers.has(identity))
|
|
129
|
-
return;
|
|
130
|
-
state.providers.set(identity, values);
|
|
131
|
-
apply(state);
|
|
132
|
-
}
|
|
133
|
-
function create(owner) {
|
|
134
|
-
const root = owner.documentElement;
|
|
135
|
-
const style = owner.createElement("style");
|
|
136
|
-
const original = new Map(Object.values(properties).map(property => [property, {
|
|
137
|
-
value: root.style.getPropertyValue(property),
|
|
138
|
-
priority: root.style.getPropertyPriority(property)
|
|
139
|
-
}]));
|
|
140
|
-
style.dataset.phreshosScrollbars = "";
|
|
141
|
-
style.textContent = stylesheet;
|
|
142
|
-
owner.head.append(style);
|
|
143
|
-
const state = { root, style, original, providers: new Map() };
|
|
144
|
-
documents.set(owner, state);
|
|
145
|
-
return state;
|
|
146
|
-
}
|
|
147
|
-
function apply(state) {
|
|
148
|
-
const values = [...state.providers.values()].at(-1);
|
|
149
|
-
if (!values)
|
|
150
|
-
return;
|
|
151
|
-
state.root.style.setProperty(properties.thumb, values.thumb);
|
|
152
|
-
state.root.style.setProperty(properties.thumbHover, values.thumbHover);
|
|
153
|
-
state.root.style.setProperty(properties.size, values.size);
|
|
154
|
-
state.root.style.setProperty(properties.padding, values.padding);
|
|
155
|
-
state.root.style.setProperty(properties.radius, values.radius);
|
|
156
|
-
}
|
|
157
|
-
function restore(state) {
|
|
158
|
-
for (const [property, original] of state.original) {
|
|
159
|
-
if (original.value)
|
|
160
|
-
state.root.style.setProperty(property, original.value, original.priority);
|
|
161
|
-
else
|
|
162
|
-
state.root.style.removeProperty(property);
|
|
163
|
-
}
|
|
164
|
-
}
|