@phreshos/react-ui 0.1.18 → 0.1.19
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 +11 -5
- package/dist/appearance-context.d.ts +18 -0
- package/dist/appearance-context.js +37 -0
- package/dist/appearance-provider.d.ts +2 -7
- package/dist/appearance-provider.js +3 -38
- package/dist/button.js +1 -2
- package/dist/color.js +3 -3
- package/dist/control-surface.js +2 -2
- package/dist/control.d.ts +1 -0
- package/dist/control.js +10 -7
- package/dist/document-scrollbars.js +2 -2
- package/dist/main.d.ts +1 -1
- package/dist/main.js +1 -1
- package/dist/material-options.js +2 -2
- package/dist/material-paint.js +2 -1
- package/dist/motion-style.d.ts +11 -19
- package/dist/motion-style.js +59 -20
- package/dist/panel.js +2 -2
- package/dist/radius.d.ts +3 -2
- package/dist/radius.js +1 -1
- package/dist/select.js +5 -3
- package/dist/slider.js +5 -4
- package/dist/spacing.js +1 -1
- package/dist/surface-edge.js +2 -1
- package/dist/surface.d.ts +2 -2
- package/dist/surface.js +7 -6
- package/dist/toggle-indicator.js +8 -7
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -92,9 +92,9 @@ wrapper. The host retains ownership of its own behavior and layout properties;
|
|
|
92
92
|
Surface retains ownership of material, edge, radius, and required geometry.
|
|
93
93
|
|
|
94
94
|
`MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
|
|
95
|
-
`distortion`, and `saturation`.
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
`distortion`, and `saturation`. Every material-bearing component, including
|
|
96
|
+
Surface, exposes these values through its `material` prop. Color remains a
|
|
97
|
+
separate property. Omitted material values follow
|
|
98
98
|
`appearance.material`. Effect options accept a scale level or a direct number;
|
|
99
99
|
opacity affects Surface paint only, never its content. The material edge is part
|
|
100
100
|
of the same Surface rather than a second public entity.
|
|
@@ -139,11 +139,11 @@ Theme names never imply particular colors. No component requires a Client or
|
|
|
139
139
|
Server SDK.
|
|
140
140
|
|
|
141
141
|
Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
|
|
142
|
-
Shared CSS transitions use
|
|
142
|
+
Shared CSS transitions use `appearance.transaction` for colors and corner
|
|
143
143
|
radius. The material fill and opacity values transition on the painted layers, not on the
|
|
144
144
|
Surface host. Select menus combine a small placement-aware slide with an overlay
|
|
145
145
|
opacity fade for entry and exit, using React Aria's animation lifecycle. Reduced-motion preferences make
|
|
146
|
-
these changes immediate
|
|
146
|
+
these changes immediate without changing the Appearance value.
|
|
147
147
|
Blur, distortion, geometry, and Surface host opacity are not transitioned; gradients
|
|
148
148
|
and structurally removed effects change directly rather than adding extra
|
|
149
149
|
layers or keeping disabled effects alive.
|
|
@@ -216,6 +216,12 @@ bun run verify
|
|
|
216
216
|
`verify` checks the contracts, tests the components, builds the package, and
|
|
217
217
|
validates its published shape.
|
|
218
218
|
|
|
219
|
+
`check` performs static checks, `build` creates distributable output, and `test`
|
|
220
|
+
runs Vitest assertions from `tests/`. Run `build` before testing built artifacts.
|
|
221
|
+
`verify` runs `check`, `build`, and `test` in order. Operational tooling belongs
|
|
222
|
+
in `scripts/`; tests and their fixtures belong in `tests/`. Verification uses
|
|
223
|
+
the committed dependency graph without local package substitutions.
|
|
224
|
+
|
|
219
225
|
## Related repositories
|
|
220
226
|
|
|
221
227
|
- [`@phreshos/core`](https://github.com/PhreshOS/core) owns Appearance, Theme,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Appearance, type Theme, type ThemedValue } from "@phreshos/core";
|
|
2
|
+
export declare const AppearanceContext: import("react").Context<Readonly<{
|
|
3
|
+
colors: ThemedValue<import("@phreshos/core").AppearanceColors>;
|
|
4
|
+
spacing: number;
|
|
5
|
+
radius: number;
|
|
6
|
+
shadow: ThemedValue<import("@phreshos/core").AppearanceShadow>;
|
|
7
|
+
material: ThemedValue<import("@phreshos/core").AppearanceMaterial>;
|
|
8
|
+
transaction: import("@phreshos/core").AppearanceTransaction;
|
|
9
|
+
signInWallpaper: ThemedValue<string | null>;
|
|
10
|
+
desktopWallpaper: ThemedValue<string | null>;
|
|
11
|
+
}>>;
|
|
12
|
+
export declare const ThemeContext: import("react").Context<Theme | null>;
|
|
13
|
+
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
14
|
+
export declare function useAppearance(): Appearance;
|
|
15
|
+
/** Returns the nearest explicit Theme, or reactively follows the browser. */
|
|
16
|
+
export declare function useTheme(): Theme;
|
|
17
|
+
/** Selects the active branch of one complete ThemedValue. */
|
|
18
|
+
export declare function useThemedValue<Value>(value: ThemedValue<Value>): Value;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
2
|
+
import { defaultAppearance } from "@phreshos/core";
|
|
3
|
+
export const AppearanceContext = createContext(defaultAppearance);
|
|
4
|
+
export const ThemeContext = createContext(null);
|
|
5
|
+
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
6
|
+
export function useAppearance() {
|
|
7
|
+
return useContext(AppearanceContext);
|
|
8
|
+
}
|
|
9
|
+
/** Returns the nearest explicit Theme, or reactively follows the browser. */
|
|
10
|
+
export function useTheme() {
|
|
11
|
+
const theme = useContext(ThemeContext);
|
|
12
|
+
const browserTheme = useBrowserTheme();
|
|
13
|
+
return theme ?? browserTheme;
|
|
14
|
+
}
|
|
15
|
+
/** Selects the active branch of one complete ThemedValue. */
|
|
16
|
+
export function useThemedValue(value) {
|
|
17
|
+
return value[useTheme()];
|
|
18
|
+
}
|
|
19
|
+
function useBrowserTheme() {
|
|
20
|
+
return useSyncExternalStore(subscribeBrowserTheme, browserTheme, serverTheme);
|
|
21
|
+
}
|
|
22
|
+
function subscribeBrowserTheme(change) {
|
|
23
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
24
|
+
return () => undefined;
|
|
25
|
+
const preference = window.matchMedia(darkThemeQuery);
|
|
26
|
+
preference.addEventListener("change", change);
|
|
27
|
+
return () => preference.removeEventListener("change", change);
|
|
28
|
+
}
|
|
29
|
+
function browserTheme() {
|
|
30
|
+
return typeof window !== "undefined"
|
|
31
|
+
&& typeof window.matchMedia === "function"
|
|
32
|
+
&& window.matchMedia(darkThemeQuery).matches
|
|
33
|
+
? "dark"
|
|
34
|
+
: "light";
|
|
35
|
+
}
|
|
36
|
+
function serverTheme() { return "light"; }
|
|
37
|
+
const darkThemeQuery = "(prefers-color-scheme: dark)";
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { type Appearance, type Theme
|
|
2
|
+
import { type Appearance, type Theme } from "@phreshos/core";
|
|
3
|
+
export { useAppearance, useTheme, useThemedValue } from "./appearance-context.js";
|
|
3
4
|
/** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
|
|
4
5
|
export declare function AppearanceProvider({ appearance, children, theme }: AppearanceProviderProps): import("react").JSX.Element;
|
|
5
|
-
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
6
|
-
export declare function useAppearance(): Appearance;
|
|
7
|
-
/** Returns the nearest explicit Theme, or reactively follows the browser. */
|
|
8
|
-
export declare function useTheme(): Theme;
|
|
9
|
-
/** Resolves one themed value only where it is consumed. */
|
|
10
|
-
export declare function useResolveTheme<Value, DarkValue extends Value = never>(value: ThemedValue<Value, DarkValue>): Exclude<Value, undefined>;
|
|
11
6
|
export interface AppearanceProviderProps {
|
|
12
7
|
readonly appearance?: Appearance;
|
|
13
8
|
readonly children: ReactNode;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { defaultAppearance } from "@phreshos/core";
|
|
2
|
+
import {} from "@phreshos/core";
|
|
4
3
|
import DocumentScrollbars from "./document-scrollbars.js";
|
|
5
4
|
import MotionStyle from "./motion-style.js";
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { AppearanceContext, ThemeContext, useAppearance, useTheme } from "./appearance-context.js";
|
|
6
|
+
export { useAppearance, useTheme, useThemedValue } from "./appearance-context.js";
|
|
8
7
|
/** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
|
|
9
8
|
export function AppearanceProvider({ appearance, children, theme }) {
|
|
10
9
|
const inheritedAppearance = useAppearance();
|
|
@@ -13,37 +12,3 @@ export function AppearanceProvider({ appearance, children, theme }) {
|
|
|
13
12
|
const resolvedTheme = theme ?? inheritedTheme;
|
|
14
13
|
return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(ThemeContext.Provider, { value: resolvedTheme, children: [_jsx(MotionStyle, {}), _jsx(DocumentScrollbars, { appearance: resolvedAppearance, theme: resolvedTheme }), children] }) });
|
|
15
14
|
}
|
|
16
|
-
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
17
|
-
export function useAppearance() {
|
|
18
|
-
return useContext(AppearanceContext);
|
|
19
|
-
}
|
|
20
|
-
/** Returns the nearest explicit Theme, or reactively follows the browser. */
|
|
21
|
-
export function useTheme() {
|
|
22
|
-
const theme = useContext(ThemeContext);
|
|
23
|
-
const browserTheme = useBrowserTheme();
|
|
24
|
-
return theme ?? browserTheme;
|
|
25
|
-
}
|
|
26
|
-
/** Resolves one themed value only where it is consumed. */
|
|
27
|
-
export function useResolveTheme(value) {
|
|
28
|
-
const theme = useTheme();
|
|
29
|
-
return (theme === "dark" && "dark" in value ? value.dark : value.light);
|
|
30
|
-
}
|
|
31
|
-
function useBrowserTheme() {
|
|
32
|
-
return useSyncExternalStore(subscribeBrowserTheme, browserTheme, serverTheme);
|
|
33
|
-
}
|
|
34
|
-
function subscribeBrowserTheme(change) {
|
|
35
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
36
|
-
return () => undefined;
|
|
37
|
-
const preference = window.matchMedia(darkThemeQuery);
|
|
38
|
-
preference.addEventListener("change", change);
|
|
39
|
-
return () => preference.removeEventListener("change", change);
|
|
40
|
-
}
|
|
41
|
-
function browserTheme() {
|
|
42
|
-
return typeof window !== "undefined"
|
|
43
|
-
&& typeof window.matchMedia === "function"
|
|
44
|
-
&& window.matchMedia(darkThemeQuery).matches
|
|
45
|
-
? "dark"
|
|
46
|
-
: "light";
|
|
47
|
-
}
|
|
48
|
-
function serverTheme() { return "light"; }
|
|
49
|
-
const darkThemeQuery = "(prefers-color-scheme: dark)";
|
package/dist/button.js
CHANGED
|
@@ -3,7 +3,6 @@ import { forwardRef } from "react";
|
|
|
3
3
|
import { Button as AriaButton } from "react-aria-components";
|
|
4
4
|
import { controlFontSizes, useControlTheme } from "./control.js";
|
|
5
5
|
import { SurfaceButton } from "./control-surface.js";
|
|
6
|
-
import { visualTransition } from "./motion-style.js";
|
|
7
6
|
/** An Appearance-aware action with normalized pointer and keyboard behavior. */
|
|
8
7
|
export const Button = forwardRef(function Button({ children, color, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, material, type = "button", ...properties }, ref) {
|
|
9
8
|
const theme = useControlTheme({ color, radius, size });
|
|
@@ -25,7 +24,7 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
|
|
|
25
24
|
const interactive = !disabled && !pending;
|
|
26
25
|
const paint = buttonPaint(theme, interactive, isHovered, isPressed);
|
|
27
26
|
return {
|
|
28
|
-
...
|
|
27
|
+
...theme.transition,
|
|
29
28
|
appearance: "none",
|
|
30
29
|
boxSizing: "border-box",
|
|
31
30
|
display: "inline-grid",
|
package/dist/color.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { ColorSpace, mix, parse, serialize, to, toGamut, contrastWCAG21, sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65 } from "colorjs.io/fn";
|
|
3
|
-
import { useAppearance,
|
|
3
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
4
4
|
// Register the CSS color spaces, without bundling unrelated color-model APIs.
|
|
5
5
|
for (const space of [sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65])
|
|
6
6
|
ColorSpace.register(space);
|
|
@@ -44,14 +44,14 @@ export function useColor(value) {
|
|
|
44
44
|
export function useResolveColor(value = defaultColor) {
|
|
45
45
|
const appearance = useAppearance();
|
|
46
46
|
const semantic = parseSemanticColor(value);
|
|
47
|
-
const source =
|
|
47
|
+
const source = useThemedValue(appearance.colors)[semantic?.name ?? "background"];
|
|
48
48
|
return semantic ? color(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
53
|
const semantic = parseSemanticColor(value);
|
|
54
|
-
const source =
|
|
54
|
+
const source = useThemedValue(appearance.colors)[semantic?.name ?? "background"];
|
|
55
55
|
return semantic ? resolveColorLevel(source, semantic.level) : opaqueColor(value);
|
|
56
56
|
}
|
|
57
57
|
/** Applies material opacity without restricting the source CSS color syntax. */
|
package/dist/control-surface.js
CHANGED
|
@@ -3,9 +3,9 @@ import { Surface } from "./surface.js";
|
|
|
3
3
|
/** A native button uses Surface without changing its native contract. */
|
|
4
4
|
export function SurfaceButton({ native, paint, material: options }) {
|
|
5
5
|
const { ref, style, children, ...properties } = native;
|
|
6
|
-
return _jsx(Surface, { ...properties,
|
|
6
|
+
return _jsx(Surface, { ...properties, as: "button", color: paint.background, material: options, ref: ref, style: { ...style, color: paint.color }, children: children });
|
|
7
7
|
}
|
|
8
8
|
/** Void text controls use a material host while retaining native sizing. */
|
|
9
9
|
export function SurfaceField({ paint, radius, children, material: options }) {
|
|
10
|
-
return _jsx(Surface, {
|
|
10
|
+
return _jsx(Surface, { as: "span", color: paint.background, material: options, radius: radius, style: { color: paint.color, display: "grid", minWidth: 0 }, children: children });
|
|
11
11
|
}
|
package/dist/control.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ControlOverrides = keyof ControlProps | keyof FieldProps | "children
|
|
|
25
25
|
export declare const controlFontSizes: Readonly<Record<ScaleLevel, number>>;
|
|
26
26
|
type SolidColors = ReturnType<typeof solidColors>;
|
|
27
27
|
export interface ControlTheme {
|
|
28
|
+
readonly transition: CSSProperties;
|
|
28
29
|
readonly spacing: number;
|
|
29
30
|
readonly foreground: string;
|
|
30
31
|
readonly background: string;
|
package/dist/control.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { FieldError, Label, Text } from "react-aria-components";
|
|
4
|
-
import { useAppearance,
|
|
4
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
5
5
|
import { resolveRadius } from "./radius.js";
|
|
6
6
|
import { scale } from "./scale.js";
|
|
7
7
|
import { solidColors } from "./color.js";
|
|
8
8
|
import { useResolveSolidColor } from "./color.js";
|
|
9
|
-
import {
|
|
9
|
+
import { useVisualTransition } from "./motion-style.js";
|
|
10
10
|
export const controlFontSizes = Object.freeze({
|
|
11
11
|
xsmall: 11,
|
|
12
12
|
small: 12,
|
|
@@ -16,9 +16,11 @@ export const controlFontSizes = Object.freeze({
|
|
|
16
16
|
});
|
|
17
17
|
export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
18
18
|
const appearance = useAppearance();
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
19
|
+
const transition = useVisualTransition();
|
|
20
|
+
const spacing = scale(appearance.spacing, size);
|
|
21
|
+
const colors = useThemedValue(appearance.colors);
|
|
22
|
+
const foreground = colors.foreground;
|
|
23
|
+
const background = colors.background;
|
|
22
24
|
const tint = useResolveSolidColor(color ?? "primary:base");
|
|
23
25
|
const danger = useResolveSolidColor("danger:base");
|
|
24
26
|
const paints = useMemo(() => ({
|
|
@@ -27,6 +29,7 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
|
27
29
|
danger: solidColors(danger, background, foreground)
|
|
28
30
|
}), [tint, background, foreground, danger]);
|
|
29
31
|
return {
|
|
32
|
+
transition,
|
|
30
33
|
spacing,
|
|
31
34
|
foreground,
|
|
32
35
|
background,
|
|
@@ -43,7 +46,7 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
|
43
46
|
export function fieldStyle(theme, disabled = false, style) {
|
|
44
47
|
return {
|
|
45
48
|
...style,
|
|
46
|
-
...
|
|
49
|
+
...theme.transition,
|
|
47
50
|
display: "grid",
|
|
48
51
|
gap: theme.gap,
|
|
49
52
|
minWidth: 0,
|
|
@@ -61,7 +64,7 @@ export function controlPaint(theme, focused, invalid, hovered = false) {
|
|
|
61
64
|
export function controlStyle(theme, focused, invalid, hovered = false, focusVisible = focused) {
|
|
62
65
|
const paint = controlPaint(theme, focused, invalid, hovered);
|
|
63
66
|
return {
|
|
64
|
-
...
|
|
67
|
+
...theme.transition,
|
|
65
68
|
appearance: "none",
|
|
66
69
|
boxSizing: "border-box",
|
|
67
70
|
width: "100%",
|
|
@@ -86,7 +86,7 @@ const documents = new WeakMap();
|
|
|
86
86
|
/** Applies one Appearance to the complete owning document without rendering. */
|
|
87
87
|
export default function DocumentScrollbars({ appearance, theme }) {
|
|
88
88
|
const identity = useRef(Symbol("AppearanceProvider")).current;
|
|
89
|
-
const foreground =
|
|
89
|
+
const foreground = appearance.colors[theme].foreground;
|
|
90
90
|
const padding = 5;
|
|
91
91
|
const thumbSize = 6;
|
|
92
92
|
const size = thumbSize + padding * 2;
|
|
@@ -95,7 +95,7 @@ export default function DocumentScrollbars({ appearance, theme }) {
|
|
|
95
95
|
thumbHover: colorOpacity(foreground, 0.2),
|
|
96
96
|
size: `${size}px`,
|
|
97
97
|
padding: `${padding}px`,
|
|
98
|
-
radius: `${Math.min(appearance.radius
|
|
98
|
+
radius: `${Math.min(appearance.radius, padding + thumbSize / 2)}px`
|
|
99
99
|
};
|
|
100
100
|
useInsertionEffect(function () {
|
|
101
101
|
if (typeof document === "undefined")
|
package/dist/main.d.ts
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, useThemedValue, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
|
|
9
9
|
export { Flex, type FlexProps } from "./flex.js";
|
|
10
10
|
export { Grid, type GridProps } from "./grid.js";
|
|
11
11
|
export { Surface, type MaterialOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.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, useThemedValue, useTheme } 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";
|
package/dist/material-options.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { appearanceLimits } from "@phreshos/core";
|
|
2
|
-
import { useAppearance,
|
|
2
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
3
3
|
import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
|
|
4
4
|
/** Resolves material values from the current Appearance without knowing any host geometry. */
|
|
5
5
|
export function useMaterialOptions(values = {}) {
|
|
6
6
|
const appearance = useAppearance();
|
|
7
|
-
const defaults =
|
|
7
|
+
const defaults = useThemedValue(appearance.material);
|
|
8
8
|
const limits = appearanceLimits.material;
|
|
9
9
|
return {
|
|
10
10
|
opacity: resolve(values.opacity, defaults.opacity, limits.opacity),
|
package/dist/material-paint.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { usePaintTransition } from "./motion-style.js";
|
|
4
4
|
/** The fill, refraction definition, and grain of one Surface. */
|
|
5
5
|
export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }) {
|
|
6
|
+
const paintTransition = usePaintTransition();
|
|
6
7
|
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
7
8
|
const hasPaint = opacity > 0;
|
|
8
9
|
const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
|
package/dist/motion-style.d.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
transitionTimingFunction: "ease-out";
|
|
12
|
-
};
|
|
13
|
-
/** Radius is inherited from the host; only the painted values transition here. */
|
|
14
|
-
export declare const paintTransition: {
|
|
15
|
-
transitionProperty: "fill, stroke, opacity";
|
|
16
|
-
transitionDuration: "var(--phreshos-ui-motion-duration, 120ms)";
|
|
17
|
-
transitionTimingFunction: "ease-out";
|
|
18
|
-
};
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { Transition } from "motion/react";
|
|
3
|
+
/** CSS timing for values painted directly by React UI. */
|
|
4
|
+
export declare function useVisualTransition(): CSSProperties;
|
|
5
|
+
/** CSS timing for SVG paint owned by a Material. */
|
|
6
|
+
export declare function usePaintTransition(): CSSProperties;
|
|
7
|
+
/** Motion timing for interactive geometry owned by React UI. */
|
|
8
|
+
export declare function useControlTransition(reduced?: boolean): Transition;
|
|
9
|
+
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
10
|
+
export declare function useOverlayTransition(): CSSProperties;
|
|
19
11
|
export declare const overlayMotionClass = "phreshos-ui-overlay";
|
|
20
|
-
/** React hoists and deduplicates this stylesheet, including
|
|
12
|
+
/** React hoists and deduplicates this stylesheet, including portals. */
|
|
21
13
|
export default function MotionStyle(): import("react").JSX.Element;
|
package/dist/motion-style.js
CHANGED
|
@@ -1,33 +1,72 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
import { defaultAppearance } from "@phreshos/core";
|
|
3
|
+
import { useAppearance } from "./appearance-context.js";
|
|
4
|
+
const visualProperties = "background-color, color, border-color, border-radius";
|
|
5
|
+
const paintProperties = "fill, stroke, opacity";
|
|
6
|
+
/** CSS timing for values painted directly by React UI. */
|
|
7
|
+
export function useVisualTransition() {
|
|
8
|
+
return { ...useTransitionTiming(), transitionProperty: visualProperties };
|
|
9
|
+
}
|
|
10
|
+
/** CSS timing for SVG paint owned by a Material. */
|
|
11
|
+
export function usePaintTransition() {
|
|
12
|
+
return { ...useTransitionTiming(), transitionProperty: paintProperties };
|
|
13
|
+
}
|
|
14
|
+
/** Motion timing for interactive geometry owned by React UI. */
|
|
15
|
+
export function useControlTransition(reduced = false) {
|
|
16
|
+
const { duration, easing } = useAppearance().transaction;
|
|
17
|
+
return {
|
|
18
|
+
type: "tween",
|
|
19
|
+
duration: reduced ? 0 : duration / 1_000,
|
|
20
|
+
ease: motionEasing(easing)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Per-overlay variables consumed by the shared entrance keyframes. */
|
|
24
|
+
export function useOverlayTransition() {
|
|
25
|
+
const { duration, easing } = useAppearance().transaction;
|
|
26
|
+
return {
|
|
27
|
+
"--phreshos-ui-motion-duration": `${duration}ms`,
|
|
28
|
+
"--phreshos-ui-motion-easing": cssEasing(easing)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
18
31
|
export const overlayMotionClass = "phreshos-ui-overlay";
|
|
32
|
+
function useTransitionTiming() {
|
|
33
|
+
const { duration, easing } = useAppearance().transaction;
|
|
34
|
+
return {
|
|
35
|
+
transitionDuration: `${duration}ms`,
|
|
36
|
+
transitionTimingFunction: cssEasing(easing)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function cssEasing(easing) {
|
|
40
|
+
return typeof easing === "string" ? easing : `cubic-bezier(${easing.join(", ")})`;
|
|
41
|
+
}
|
|
42
|
+
function motionEasing(easing) {
|
|
43
|
+
if (typeof easing !== "string")
|
|
44
|
+
return [...easing];
|
|
45
|
+
if (easing === "ease")
|
|
46
|
+
return [0.25, 0.1, 0.25, 1];
|
|
47
|
+
if (easing === "ease-in")
|
|
48
|
+
return "easeIn";
|
|
49
|
+
if (easing === "ease-out")
|
|
50
|
+
return "easeOut";
|
|
51
|
+
if (easing === "ease-in-out")
|
|
52
|
+
return "easeInOut";
|
|
53
|
+
return easing;
|
|
54
|
+
}
|
|
19
55
|
const stylesheet = `
|
|
20
|
-
:root {
|
|
56
|
+
:root {
|
|
57
|
+
--phreshos-ui-motion-duration: ${defaultAppearance.transaction.duration}ms;
|
|
58
|
+
--phreshos-ui-motion-easing: ${cssEasing(defaultAppearance.transaction.easing)};
|
|
59
|
+
}
|
|
21
60
|
|
|
22
61
|
.phreshos-ui-overlay { --phreshos-ui-overlay-x: 0px; --phreshos-ui-overlay-y: -4px; }
|
|
23
62
|
.phreshos-ui-overlay[data-placement="top"] { --phreshos-ui-overlay-y: 4px; }
|
|
24
63
|
.phreshos-ui-overlay[data-placement="left"] { --phreshos-ui-overlay-x: 4px; --phreshos-ui-overlay-y: 0px; }
|
|
25
64
|
.phreshos-ui-overlay[data-placement="right"] { --phreshos-ui-overlay-x: -4px; --phreshos-ui-overlay-y: 0px; }
|
|
26
65
|
.phreshos-ui-overlay[data-entering] {
|
|
27
|
-
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration
|
|
66
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) both;
|
|
28
67
|
}
|
|
29
68
|
.phreshos-ui-overlay[data-exiting] {
|
|
30
|
-
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration
|
|
69
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) reverse both;
|
|
31
70
|
pointer-events: none;
|
|
32
71
|
}
|
|
33
72
|
@keyframes phreshos-ui-overlay-enter {
|
|
@@ -39,7 +78,7 @@ const stylesheet = `
|
|
|
39
78
|
.phreshos-ui-overlay[data-entering], .phreshos-ui-overlay[data-exiting] { animation: none; }
|
|
40
79
|
}
|
|
41
80
|
`;
|
|
42
|
-
/** React hoists and deduplicates this stylesheet, including
|
|
81
|
+
/** React hoists and deduplicates this stylesheet, including portals. */
|
|
43
82
|
export default function MotionStyle() {
|
|
44
83
|
return _jsx("style", { href: "phreshos-react-ui-motion", precedence: "phreshos", children: stylesheet });
|
|
45
84
|
}
|
package/dist/panel.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
|
-
import { useAppearance
|
|
3
|
+
import { useAppearance } from "./appearance-provider.js";
|
|
4
4
|
import { scale } from "./scale.js";
|
|
5
5
|
import { Surface } from "./surface.js";
|
|
6
6
|
/** Owns the shared shell layout, not positioning, interaction, or lifecycle. */
|
|
7
7
|
export const Panel = forwardRef(function Panel({ header, children, contentProps, style, ...properties }, ref) {
|
|
8
8
|
const appearance = useAppearance();
|
|
9
|
-
const inset = scale(
|
|
9
|
+
const inset = scale(appearance.spacing, "small");
|
|
10
10
|
const hasHeader = header !== undefined && header !== null && header !== false;
|
|
11
11
|
return _jsxs(Surface, { ...properties, ref: ref, style: {
|
|
12
12
|
display: "grid",
|
package/dist/radius.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
|
-
import type { Appearance
|
|
2
|
+
import type { Appearance } from "@phreshos/core";
|
|
3
3
|
import { type ScaleLevel } from "./scale.js";
|
|
4
4
|
/** An Appearance-derived level, pixel value, or explicit CSS corner radius. */
|
|
5
5
|
export type Radius = ScaleLevel | number | (string & {});
|
|
6
6
|
/** Shared semantic corner-radius capability for React UI components. */
|
|
7
|
-
export interface RadiusProps
|
|
7
|
+
export interface RadiusProps {
|
|
8
|
+
readonly radius?: Radius;
|
|
8
9
|
}
|
|
9
10
|
/** Resolves a Radius while preserving explicit CSS and pixel values. */
|
|
10
11
|
export declare function resolveRadius(value: Radius | undefined, appearance: Appearance | null): CSSProperties["borderRadius"];
|
package/dist/radius.js
CHANGED
package/dist/select.js
CHANGED
|
@@ -5,17 +5,19 @@ import { Select as AriaSelect, Button, SelectValue, Popover, ListBox, ListBoxIte
|
|
|
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 {
|
|
8
|
+
import { overlayMotionClass, useControlTransition, useOverlayTransition } from "./motion-style.js";
|
|
9
9
|
/** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
|
|
10
10
|
export const Select = forwardRef(function Select({ label, description, errorMessage, disabled, required, invalid, options, value, defaultValue, onChange, size, color, radius, style, material, ...properties }, ref) {
|
|
11
11
|
const theme = useControlTheme({ size, color, radius });
|
|
12
12
|
const reduced = useReducedMotion();
|
|
13
|
+
const transition = useControlTransition(Boolean(reduced));
|
|
14
|
+
const overlayTransition = useOverlayTransition();
|
|
13
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 => ({
|
|
14
16
|
...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
|
|
15
17
|
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
16
18
|
cursor: disabled ? "not-allowed" : "pointer", textAlign: "start"
|
|
17
|
-
}), 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:
|
|
18
|
-
...
|
|
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, minHeight: 0, overflow: "auto", outline: "none", fontSize: theme.fontSize, color: theme.foreground }, children: option => _jsx(ListBoxItem, { id: option.value, textValue: option.label, style: item => ({
|
|
20
|
+
...theme.transition,
|
|
19
21
|
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
20
22
|
minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
|
|
21
23
|
borderRadius: theme.radius,
|
package/dist/slider.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useId } from "react";
|
|
|
3
3
|
import { motion, useReducedMotion } 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
|
-
import {
|
|
6
|
+
import { useControlTransition } from "./motion-style.js";
|
|
7
7
|
/** One numeric value with pointer, touch, keyboard, and form support. */
|
|
8
8
|
export const Slider = forwardRef(function Slider({ label, description, name, disabled, size, color, style, orientation = "horizontal", ...properties }, ref) {
|
|
9
9
|
const theme = useControlTheme({ size, color });
|
|
@@ -11,17 +11,18 @@ export const Slider = forwardRef(function Slider({ label, description, name, dis
|
|
|
11
11
|
const vertical = orientation === "vertical";
|
|
12
12
|
const diameter = theme.fontSize + 6;
|
|
13
13
|
const reduced = useReducedMotion();
|
|
14
|
+
const transition = useControlTransition(Boolean(reduced));
|
|
14
15
|
const rail = Math.max(4, Math.round(diameter / 3));
|
|
15
16
|
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: {
|
|
16
17
|
position: "relative", touchAction: "none",
|
|
17
18
|
width: vertical ? theme.height : "100%", height: vertical ? 160 : theme.height
|
|
18
19
|
}, children: [_jsx("div", { "aria-hidden": "true", style: {
|
|
19
|
-
...
|
|
20
|
+
...theme.transition,
|
|
20
21
|
position: "absolute", borderRadius: rail,
|
|
21
22
|
...(vertical ? { width: rail, height: "100%", left: "50%", transform: "translateX(-50%)" } : { height: rail, width: "100%", top: "50%", transform: "translateY(-50%)" }),
|
|
22
23
|
background: theme.paints.neutral.rest.background
|
|
23
24
|
} }), _jsx(SliderFill, { style: {
|
|
24
|
-
...
|
|
25
|
+
...theme.transition,
|
|
25
26
|
position: "absolute", background: theme.paints.palette.rest.background, borderRadius: rail,
|
|
26
27
|
...(vertical ? { width: rail, left: "50%", transform: "translateX(-50%)" } : { height: rail, top: "50%", transform: "translateY(-50%)" })
|
|
27
28
|
} }), _jsx(SliderThumb, { name: name, style: state => ({
|
|
@@ -29,7 +30,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, dis
|
|
|
29
30
|
...(vertical ? { left: "50%" } : { top: "50%" }),
|
|
30
31
|
outline: state.isFocusVisible ? `2px solid ${theme.foreground}` : "none", outlineOffset: 2,
|
|
31
32
|
cursor: state.isDisabled ? "not-allowed" : state.isDragging ? "grabbing" : "grab"
|
|
32
|
-
}), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: reduced || state.isDisabled ? 1 : state.isDragging ? 0.92 : state.isHovered ? 1.08 : 1 }, transition:
|
|
33
|
+
}), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: reduced || 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",
|
|
33
34
|
background: (state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).color,
|
|
34
35
|
border: `3px solid ${(state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).background}`,
|
|
35
36
|
boxSizing: "border-box" } }) })] }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
|
package/dist/spacing.js
CHANGED
package/dist/surface-edge.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { colorLightness, colorOpacity, orderColors } from "./color.js";
|
|
4
|
-
import {
|
|
4
|
+
import { usePaintTransition } from "./motion-style.js";
|
|
5
5
|
import { scale } from "./scale.js";
|
|
6
6
|
/** Paints the boundary owned by one Surface from the material it contains. */
|
|
7
7
|
export function SurfaceEdge({ material }) {
|
|
8
|
+
const paintTransition = usePaintTransition();
|
|
8
9
|
const visible = material.opacity > 0;
|
|
9
10
|
const palette = useRef(null);
|
|
10
11
|
const [colors, setColors] = useState(null);
|
package/dist/surface.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactElement, ReactNode } from "react";
|
|
2
2
|
import { type Color } from "./color.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type MaterialOverrides } from "./material-options.js";
|
|
4
4
|
import { type RadiusProps } from "./radius.js";
|
|
5
5
|
export type { MaterialOptions } from "./material-options.js";
|
|
6
|
-
export interface SurfaceOwnProps extends
|
|
6
|
+
export interface SurfaceOwnProps extends MaterialOverrides, RadiusProps {
|
|
7
7
|
readonly color?: Color;
|
|
8
8
|
}
|
|
9
9
|
/**
|
package/dist/surface.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createElement, forwardRef, useId } from "react";
|
|
3
|
-
import { useAppearance,
|
|
3
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
4
4
|
import { useResolveColor } from "./color.js";
|
|
5
5
|
import { useMaterialOptions } from "./material-options.js";
|
|
6
6
|
import { MaterialPaint } from "./material-paint.js";
|
|
7
|
-
import MotionStyle, {
|
|
7
|
+
import MotionStyle, { useVisualTransition } from "./motion-style.js";
|
|
8
8
|
import { resolveRadius } from "./radius.js";
|
|
9
9
|
import { SurfaceEdge } from "./surface-edge.js";
|
|
10
10
|
/** One material-owning element. The host is a div unless `as` selects another element. */
|
|
11
|
-
const SurfaceRoot = forwardRef(function Surface({ as: Element = "div", color,
|
|
11
|
+
const SurfaceRoot = forwardRef(function Surface({ as: Element = "div", color, material: options, radius, children, style, ...properties }, ref) {
|
|
12
12
|
const appearance = useAppearance();
|
|
13
|
-
const
|
|
13
|
+
const transition = useVisualTransition();
|
|
14
|
+
const material = useResolvedSurface(color, options);
|
|
14
15
|
const resolvedRadius = resolveRadius(radius ?? "medium", appearance);
|
|
15
16
|
return createElement(Element, {
|
|
16
17
|
...properties,
|
|
17
18
|
ref,
|
|
18
19
|
style: {
|
|
19
|
-
...
|
|
20
|
+
...transition,
|
|
20
21
|
...style,
|
|
21
22
|
background: "transparent",
|
|
22
23
|
color: style?.color ?? material.foreground,
|
|
@@ -33,7 +34,7 @@ function useResolvedSurface(color, options) {
|
|
|
33
34
|
return {
|
|
34
35
|
...material,
|
|
35
36
|
color: useResolveColor(color),
|
|
36
|
-
foreground:
|
|
37
|
+
foreground: useThemedValue(appearance.colors).foreground
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
const layerStyle = {
|
package/dist/toggle-indicator.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { motion, useReducedMotion } from "motion/react";
|
|
3
3
|
import { useLocale } from "react-aria-components";
|
|
4
|
-
import {
|
|
4
|
+
import { useControlTransition } from "./motion-style.js";
|
|
5
5
|
import { Surface } from "./surface.js";
|
|
6
6
|
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
7
7
|
export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
|
|
8
8
|
const reduced = useReducedMotion();
|
|
9
|
+
const transition = useControlTransition(Boolean(reduced));
|
|
9
10
|
const { direction } = useLocale();
|
|
10
11
|
const diameter = theme.fontSize + 6;
|
|
11
12
|
const active = selected || indeterminate;
|
|
@@ -15,9 +16,9 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
|
|
|
15
16
|
const switching = kind === "switch";
|
|
16
17
|
const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
|
|
17
18
|
const travel = (diameter * 2 - thumbWidth) / 2 - 3;
|
|
18
|
-
return _jsx(Surface, { as: motion.span,
|
|
19
|
+
return _jsx(Surface, { as: motion.span, material: options, color: paint.background, "aria-hidden": "true", initial: false, animate: { scale: reduced || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: transition, style: {
|
|
19
20
|
color: paint.color,
|
|
20
|
-
...
|
|
21
|
+
...theme.transition,
|
|
21
22
|
display: "inline-grid",
|
|
22
23
|
placeItems: "center",
|
|
23
24
|
position: "relative",
|
|
@@ -29,12 +30,12 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
|
|
|
29
30
|
border: "none",
|
|
30
31
|
outline: focused ? `2px solid ${theme.foreground}` : "none",
|
|
31
32
|
outlineOffset: 2,
|
|
32
|
-
}, children: switching ? _jsx(motion.span, { initial: false, animate: { width: thumbWidth, x: (selected ? travel : -travel) * (direction === "rtl" ? -1 : 1) }, transition:
|
|
33
|
-
...
|
|
33
|
+
}, children: switching ? _jsx(motion.span, { initial: false, animate: { width: thumbWidth, x: (selected ? travel : -travel) * (direction === "rtl" ? -1 : 1) }, transition: transition, style: {
|
|
34
|
+
...theme.transition,
|
|
34
35
|
height: diameter - 6, borderRadius: diameter,
|
|
35
36
|
background: paint.color
|
|
36
|
-
} }) : _jsx(motion.span, { initial: false, animate: { opacity: active ? 1 : 0, scale: active ? 1 : 0.7 }, transition:
|
|
37
|
-
: _jsxs("svg", { width: diameter - 4, height: diameter - 4, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx(motion.path, { d: "m3 8 3 3 7-7", initial: false, animate: { pathLength: selected && !indeterminate ? 1 : 0, opacity: selected && !indeterminate ? 1 : 0 }, transition:
|
|
37
|
+
} }) : _jsx(motion.span, { initial: false, animate: { opacity: active ? 1 : 0, scale: active ? 1 : 0.7 }, transition: transition, style: { display: "grid", placeItems: "center" }, children: kind === "radio" ? _jsx("span", { style: { ...theme.transition, width: diameter / 2, height: diameter / 2, borderRadius: "50%", background: "currentColor" } })
|
|
38
|
+
: _jsxs("svg", { width: diameter - 4, height: diameter - 4, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx(motion.path, { d: "m3 8 3 3 7-7", initial: false, animate: { pathLength: selected && !indeterminate ? 1 : 0, opacity: selected && !indeterminate ? 1 : 0 }, transition: transition }), _jsx(motion.path, { d: "M4 8h8", initial: false, animate: { pathLength: indeterminate ? 1 : 0, opacity: indeterminate ? 1 : 0 }, transition: transition })] }) }) });
|
|
38
39
|
}
|
|
39
40
|
export function toggleStyle(theme, disabled, readOnly) {
|
|
40
41
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,18 +45,17 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
47
47
|
"check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
48
|
-
"test": "vitest run --
|
|
48
|
+
"test": "vitest run --project default",
|
|
49
49
|
"build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
|
|
50
|
-
"verify
|
|
51
|
-
"
|
|
52
|
-
"prepack": "node --run test && node --run build"
|
|
50
|
+
"verify": "node --run check && node --run build && node --run test",
|
|
51
|
+
"prepack": "node --run build"
|
|
53
52
|
},
|
|
54
53
|
"peerDependencies": {
|
|
55
54
|
"react": "^19.2.0",
|
|
56
55
|
"react-dom": "^19.2.0"
|
|
57
56
|
},
|
|
58
57
|
"dependencies": {
|
|
59
|
-
"@phreshos/core": "^0.1.
|
|
58
|
+
"@phreshos/core": "^0.1.43",
|
|
60
59
|
"colorjs.io": "^0.7.1",
|
|
61
60
|
"motion": "^13.2.0",
|
|
62
61
|
"react-aria-components": "^1.20.0"
|
|
@@ -72,6 +71,6 @@
|
|
|
72
71
|
"react": "^19.2.8",
|
|
73
72
|
"react-dom": "^19.2.8",
|
|
74
73
|
"typescript": "^6.0.3",
|
|
75
|
-
"vitest": "^4.1.
|
|
74
|
+
"vitest": "^4.1.10"
|
|
76
75
|
}
|
|
77
76
|
}
|