@phreshos/react-ui 0.1.17 → 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 +33 -33
- 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.d.ts +1 -1
- package/dist/button.js +3 -4
- package/dist/checkbox.d.ts +1 -1
- package/dist/color.js +3 -3
- package/dist/{control-material.d.ts → control-surface.d.ts} +3 -3
- package/dist/control-surface.js +11 -0
- package/dist/control.d.ts +1 -0
- package/dist/control.js +10 -7
- package/dist/document-scrollbars.js +2 -2
- package/dist/input.js +2 -2
- package/dist/main.d.ts +2 -3
- package/dist/main.js +1 -2
- package/dist/material-options.d.ts +5 -1
- package/dist/material-options.js +3 -3
- package/dist/material-paint.d.ts +1 -1
- package/dist/material-paint.js +3 -2
- package/dist/motion-style.d.ts +11 -19
- package/dist/motion-style.js +59 -20
- package/dist/panel.js +2 -2
- package/dist/radio.d.ts +1 -1
- package/dist/radius.d.ts +3 -2
- package/dist/radius.js +1 -1
- package/dist/select.d.ts +1 -1
- package/dist/select.js +7 -5
- package/dist/slider.js +5 -4
- package/dist/spacing.js +1 -1
- package/dist/surface-edge.d.ts +5 -2
- package/dist/surface-edge.js +2 -1
- package/dist/surface.d.ts +21 -5
- package/dist/surface.js +53 -11
- package/dist/switch.d.ts +1 -1
- package/dist/text-control.d.ts +1 -1
- package/dist/textarea.js +2 -2
- package/dist/toggle-indicator.js +11 -13
- package/package.json +6 -7
- package/dist/control-material.js +0 -14
- package/dist/material.d.ts +0 -27
- package/dist/material.js +0 -45
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ import { AppearanceProvider, Button } from "@phreshos/react-ui"
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
`Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `Radio`
|
|
57
|
-
|
|
57
|
+
use the shared Surface implementation while retaining their native behavior. Color and material
|
|
58
58
|
are independent inputs. `color` accepts an Appearance color and resting level such
|
|
59
|
-
as `background:base` or `primary:soft`, or a direct CSS color.
|
|
59
|
+
as `background:base` or `primary:soft`, or a direct CSS color. Surface and neutral
|
|
60
60
|
surface hosts default to `background:base`; a component may select a semantic
|
|
61
61
|
default required by its own behavior, such as `primary:base` for selection.
|
|
62
62
|
Hover and press derive shades of that fill. The resting fill chooses the
|
|
@@ -72,40 +72,34 @@ spacing and radius follow Appearance. `disabled` prevents activation and focus;
|
|
|
72
72
|
<Button color="danger:soft" size="small">Delete</Button>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<div style={{ position: "relative", isolation: "isolate", width: 240, height: 120, borderRadius: 18 }}>
|
|
82
|
-
<Material color="background:soft" material={{ opacity: 0.4 }} />
|
|
83
|
-
</div>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
`Surface` is the standard geometric host for a `Material`. It establishes the
|
|
87
|
-
required positioning and isolation on a `div`, resolves radius, renders Material,
|
|
88
|
-
and then renders its content. It never creates or consumes a shadow. Shadow remains
|
|
89
|
-
an independent visual concern. `radius` accepts a size level, a number in pixels,
|
|
90
|
-
or a CSS radius and defaults to `medium`.
|
|
75
|
+
`Surface` is the material-owning element. It renders a `div` by default, while
|
|
76
|
+
`as` selects another React element and preserves that element's native properties
|
|
77
|
+
and ref type. Surface owns its paint, opacity, frost, refraction, grain, edge,
|
|
78
|
+
radius, and clipping requirements. It never creates or consumes a shadow; shadow
|
|
79
|
+
remains an independent visual concern. `radius` accepts a size level, a number in
|
|
80
|
+
pixels, or a CSS radius and defaults to `medium`.
|
|
91
81
|
|
|
92
82
|
```tsx
|
|
93
83
|
<Surface color="background:soft" radius="large">Derived values</Surface>
|
|
94
|
-
<Surface color="#345678" radius={18}>
|
|
84
|
+
<Surface as="button" type="button" color="#345678" radius={18}>Action</Surface>
|
|
85
|
+
<Surface as={Grid} columns={3} gap="medium">Grid content</Surface>
|
|
95
86
|
```
|
|
96
87
|
|
|
97
|
-
`
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
only, never the host's children.
|
|
103
|
-
|
|
104
|
-
Radius belongs to host geometry, while Surface's border belongs to its geometric
|
|
105
|
-
boundary. Material may determine the border treatment, but it neither owns nor
|
|
106
|
-
renders that border.
|
|
88
|
+
`as` can also select an outside React component. A valid Surface host preserves
|
|
89
|
+
the `style` and `children` it receives on one host element and forwards its ref
|
|
90
|
+
to that same element. This lets layout components carry the material without a
|
|
91
|
+
wrapper. The host retains ownership of its own behavior and layout properties;
|
|
92
|
+
Surface retains ownership of material, edge, radius, and required geometry.
|
|
107
93
|
|
|
108
|
-
|
|
94
|
+
`MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
|
|
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
|
+
`appearance.material`. Effect options accept a scale level or a direct number;
|
|
99
|
+
opacity affects Surface paint only, never its content. The material edge is part
|
|
100
|
+
of the same Surface rather than a second public entity.
|
|
101
|
+
|
|
102
|
+
Surface-based controls expose the same separate `color` and `material` props.
|
|
109
103
|
For text fields and Select, `material` targets the field or trigger; for Checkbox,
|
|
110
104
|
Switch, and Radio, it targets the indicator. RadioGroup supplies material defaults
|
|
111
105
|
to its options, and a Radio can override them.
|
|
@@ -145,11 +139,11 @@ Theme names never imply particular colors. No component requires a Client or
|
|
|
145
139
|
Server SDK.
|
|
146
140
|
|
|
147
141
|
Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
|
|
148
|
-
Shared CSS transitions use
|
|
149
|
-
radius.
|
|
142
|
+
Shared CSS transitions use `appearance.transaction` for colors and corner
|
|
143
|
+
radius. The material fill and opacity values transition on the painted layers, not on the
|
|
150
144
|
Surface host. Select menus combine a small placement-aware slide with an overlay
|
|
151
145
|
opacity fade for entry and exit, using React Aria's animation lifecycle. Reduced-motion preferences make
|
|
152
|
-
these changes immediate
|
|
146
|
+
these changes immediate without changing the Appearance value.
|
|
153
147
|
Blur, distortion, geometry, and Surface host opacity are not transitioned; gradients
|
|
154
148
|
and structurally removed effects change directly rather than adding extra
|
|
155
149
|
layers or keeping disabled effects alive.
|
|
@@ -222,6 +216,12 @@ bun run verify
|
|
|
222
216
|
`verify` checks the contracts, tests the components, builds the package, and
|
|
223
217
|
validates its published shape.
|
|
224
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
|
+
|
|
225
225
|
## Related repositories
|
|
226
226
|
|
|
227
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.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
|
|
3
3
|
import type { ScaleLevel } from "./scale.js";
|
|
4
4
|
import type { RadiusProps } from "./radius.js";
|
|
5
5
|
import { type ControlColor } from "./control.js";
|
|
6
|
-
import type { MaterialOverrides } from "./material.js";
|
|
6
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
7
7
|
type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
|
|
8
8
|
/** A semantic color from Appearance; omission uses background and foreground. */
|
|
9
9
|
export type ButtonColor = ControlColor;
|
package/dist/button.js
CHANGED
|
@@ -2,12 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { Button as AriaButton } from "react-aria-components";
|
|
4
4
|
import { controlFontSizes, useControlTheme } from "./control.js";
|
|
5
|
-
import {
|
|
6
|
-
import { visualTransition } from "./motion-style.js";
|
|
5
|
+
import { SurfaceButton } from "./control-surface.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 });
|
|
10
|
-
return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, render: (native, state) => _jsx(
|
|
9
|
+
return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, render: (native, state) => _jsx(SurfaceButton, { native: native, material: material, paint: buttonPaint(theme, !disabled && !pending, state.isHovered, state.isPressed) }), style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
|
|
11
10
|
theme,
|
|
12
11
|
disabled,
|
|
13
12
|
isFocusVisible,
|
|
@@ -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/checkbox.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CheckboxFieldProps } from "react-aria-components";
|
|
2
2
|
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
|
-
import type { MaterialOverrides } from "./material.js";
|
|
3
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
4
4
|
export interface CheckboxProps extends Omit<CheckboxFieldProps, ControlOverrides | "isReadOnly" | "isSelected" | "defaultSelected" | "isIndeterminate">, ControlProps, FieldProps, MaterialOverrides {
|
|
5
5
|
readonly checked?: boolean;
|
|
6
6
|
readonly defaultChecked?: boolean;
|
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. */
|
|
@@ -4,14 +4,14 @@ type Paint = Readonly<{
|
|
|
4
4
|
background: string;
|
|
5
5
|
color: string;
|
|
6
6
|
}>;
|
|
7
|
-
/** A native button
|
|
8
|
-
export declare function
|
|
7
|
+
/** A native button uses Surface without changing its native contract. */
|
|
8
|
+
export declare function SurfaceButton({ native, paint, material: options }: {
|
|
9
9
|
readonly native: ComponentPropsWithRef<"button">;
|
|
10
10
|
readonly paint: Paint;
|
|
11
11
|
readonly material?: MaterialOptions;
|
|
12
12
|
}): import("react").JSX.Element;
|
|
13
13
|
/** Void text controls use a material host while retaining native sizing. */
|
|
14
|
-
export declare function
|
|
14
|
+
export declare function SurfaceField({ paint, radius, children, material: options }: Readonly<{
|
|
15
15
|
paint: Paint;
|
|
16
16
|
radius: CSSProperties["borderRadius"];
|
|
17
17
|
children: ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Surface } from "./surface.js";
|
|
3
|
+
/** A native button uses Surface without changing its native contract. */
|
|
4
|
+
export function SurfaceButton({ native, paint, material: options }) {
|
|
5
|
+
const { ref, style, children, ...properties } = native;
|
|
6
|
+
return _jsx(Surface, { ...properties, as: "button", color: paint.background, material: options, ref: ref, style: { ...style, color: paint.color }, children: children });
|
|
7
|
+
}
|
|
8
|
+
/** Void text controls use a material host while retaining native sizing. */
|
|
9
|
+
export function SurfaceField({ paint, radius, children, material: options }) {
|
|
10
|
+
return _jsx(Surface, { as: "span", color: paint.background, material: options, radius: radius, style: { color: paint.color, display: "grid", minWidth: 0 }, children: children });
|
|
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/input.js
CHANGED
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { Input as AriaInput, TextField } from "react-aria-components";
|
|
4
4
|
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SurfaceField } from "./control-surface.js";
|
|
6
6
|
import FieldStyle, { textControlClass } from "./field-style.js";
|
|
7
7
|
/** A labeled single-line text control. onChange receives the string value. */
|
|
8
8
|
export const Input = forwardRef(function Input({ label, description, errorMessage, disabled, readOnly, required, invalid, size, color, radius, style, className, placeholder, material, ...properties }, ref) {
|
|
9
9
|
const theme = useControlTheme({ size, color, radius });
|
|
10
|
-
return _jsxs(TextField, { ...properties, className: className, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldStyle, {}), _jsx(FieldLabel, { label: label }), _jsx(AriaInput, { ref: ref, placeholder: placeholder, className: textControlClass, render: (native, state) => _jsx(
|
|
10
|
+
return _jsxs(TextField, { ...properties, className: className, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldStyle, {}), _jsx(FieldLabel, { label: label }), _jsx(AriaInput, { ref: ref, placeholder: placeholder, className: textControlClass, render: (native, state) => _jsx(SurfaceField, { material: material, radius: theme.radius, paint: controlPaint(theme, state.isFocused, state.isInvalid, state.isHovered), children: _jsx("input", { ...native, style: { ...native.style, borderRadius: "inherit", background: "transparent" } }) }), style: state => controlStyle(theme, state.isFocused, state.isInvalid, state.isHovered, state.isFocusVisible) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
|
|
11
11
|
});
|
package/dist/main.d.ts
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
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
|
-
export {
|
|
12
|
-
export { Surface, type SurfaceProps } from "./surface.js";
|
|
11
|
+
export { Surface, type MaterialOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
|
|
13
12
|
export { defaultColor, type Color, type ColorLevel, type ColorScale } from "./color.js";
|
|
14
13
|
export { Panel, type PanelProps } from "./panel.js";
|
|
15
14
|
export { Button, type ButtonColor, type ButtonProps } from "./button.js";
|
package/dist/main.js
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
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
|
-
export { Material } from "./material.js";
|
|
12
11
|
export { Surface } from "./surface.js";
|
|
13
12
|
export { defaultColor } from "./color.js";
|
|
14
13
|
export { Panel } from "./panel.js";
|
|
@@ -8,7 +8,11 @@ export interface MaterialOptions {
|
|
|
8
8
|
readonly distortion?: ScaleLevel | number;
|
|
9
9
|
readonly saturation?: ScaleLevel | number;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/** Grouped material customization exposed by components built on Surface. */
|
|
12
|
+
export interface MaterialOverrides {
|
|
13
|
+
readonly material?: MaterialOptions;
|
|
14
|
+
}
|
|
15
|
+
/** Resolves material values from the current Appearance without knowing any host geometry. */
|
|
12
16
|
export declare function useMaterialOptions(values?: MaterialOptions): {
|
|
13
17
|
opacity: number;
|
|
14
18
|
backdrop: number;
|
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
|
-
/** Resolves
|
|
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.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ interface MaterialPaintProps {
|
|
|
6
6
|
readonly identity: string;
|
|
7
7
|
readonly opacity: number;
|
|
8
8
|
}
|
|
9
|
-
/** The fill, refraction definition, and grain of one
|
|
9
|
+
/** The fill, refraction definition, and grain of one Surface. */
|
|
10
10
|
export declare function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }: MaterialPaintProps): import("react").JSX.Element | null;
|
|
11
11
|
export {};
|
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 {
|
|
4
|
-
/** The fill, refraction definition, and grain of one
|
|
3
|
+
import { usePaintTransition } from "./motion-style.js";
|
|
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/radio.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { RadioGroupProps as AriaRadioGroupProps, RadioFieldProps } from "react-aria-components";
|
|
3
3
|
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
4
|
-
import type { MaterialOverrides } from "./material.js";
|
|
4
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
5
5
|
export interface RadioGroupProps extends Omit<AriaRadioGroupProps, ControlOverrides | "isReadOnly">, ControlProps, FieldProps, MaterialOverrides {
|
|
6
6
|
readonly children: ReactNode;
|
|
7
7
|
readonly readOnly?: boolean;
|
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.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SelectProps as AriaSelectProps } from "react-aria-components";
|
|
2
2
|
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
3
|
import type { RadiusProps } from "./radius.js";
|
|
4
|
-
import type { MaterialOverrides } from "./material.js";
|
|
4
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
5
5
|
export interface SelectOption {
|
|
6
6
|
readonly value: string;
|
|
7
7
|
readonly label: string;
|
package/dist/select.js
CHANGED
|
@@ -3,19 +3,21 @@ import { forwardRef } from "react";
|
|
|
3
3
|
import { motion, useReducedMotion } 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
|
-
import {
|
|
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
|
-
|
|
13
|
+
const transition = useControlTransition(Boolean(reduced));
|
|
14
|
+
const overlayTransition = useOverlayTransition();
|
|
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.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type EdgeMaterial = Readonly<{
|
|
2
|
+
color: string;
|
|
3
|
+
foreground: string;
|
|
4
|
+
opacity: number;
|
|
5
|
+
}>;
|
|
3
6
|
/** Paints the boundary owned by one Surface from the material it contains. */
|
|
4
7
|
export declare function SurfaceEdge({ material }: Readonly<{
|
|
5
8
|
material: EdgeMaterial;
|
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,7 +1,23 @@
|
|
|
1
|
-
import type { ComponentPropsWithRef } from "react";
|
|
2
|
-
import { type
|
|
1
|
+
import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type Color } from "./color.js";
|
|
3
|
+
import { type MaterialOverrides } from "./material-options.js";
|
|
3
4
|
import { type RadiusProps } from "./radius.js";
|
|
4
|
-
|
|
5
|
-
export interface
|
|
5
|
+
export type { MaterialOptions } from "./material-options.js";
|
|
6
|
+
export interface SurfaceOwnProps extends MaterialOverrides, RadiusProps {
|
|
7
|
+
readonly color?: Color;
|
|
6
8
|
}
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The structural properties a component must preserve when it hosts a Surface.
|
|
11
|
+
* The component must apply `style` and render `children` on the same host element,
|
|
12
|
+
* and it must forward its ref to that element.
|
|
13
|
+
*/
|
|
14
|
+
export interface SurfaceHostProps {
|
|
15
|
+
readonly children?: ReactNode;
|
|
16
|
+
readonly style?: CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
export type SurfaceHost = ElementType<SurfaceHostProps>;
|
|
19
|
+
export type SurfaceProps<As extends SurfaceHost = "div"> = SurfaceOwnProps & Readonly<{
|
|
20
|
+
as?: As;
|
|
21
|
+
}> & Omit<ComponentPropsWithRef<As>, keyof SurfaceOwnProps | "as" | "color">;
|
|
22
|
+
export type SurfaceComponent = <As extends SurfaceHost = "div">(properties: SurfaceProps<As>) => ReactElement | null;
|
|
23
|
+
export declare const Surface: SurfaceComponent;
|
package/dist/surface.js
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from "react";
|
|
3
|
-
import { useAppearance } from "./appearance-provider.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createElement, forwardRef, useId } from "react";
|
|
3
|
+
import { useAppearance, useThemedValue } from "./appearance-provider.js";
|
|
4
|
+
import { useResolveColor } from "./color.js";
|
|
5
|
+
import { useMaterialOptions } from "./material-options.js";
|
|
6
|
+
import { MaterialPaint } from "./material-paint.js";
|
|
7
|
+
import MotionStyle, { useVisualTransition } from "./motion-style.js";
|
|
6
8
|
import { resolveRadius } from "./radius.js";
|
|
7
9
|
import { SurfaceEdge } from "./surface-edge.js";
|
|
8
|
-
|
|
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, material: options, radius, children, style, ...properties }, ref) {
|
|
9
12
|
const appearance = useAppearance();
|
|
10
|
-
const
|
|
13
|
+
const transition = useVisualTransition();
|
|
14
|
+
const material = useResolvedSurface(color, options);
|
|
11
15
|
const resolvedRadius = resolveRadius(radius ?? "medium", appearance);
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
return createElement(Element, {
|
|
17
|
+
...properties,
|
|
18
|
+
ref,
|
|
19
|
+
style: {
|
|
20
|
+
...transition,
|
|
16
21
|
...style,
|
|
22
|
+
background: "transparent",
|
|
23
|
+
color: style?.color ?? material.foreground,
|
|
17
24
|
borderRadius: radius === undefined ? style?.borderRadius ?? resolvedRadius : resolvedRadius,
|
|
18
25
|
position: style?.position ?? "relative",
|
|
19
26
|
isolation: "isolate"
|
|
20
|
-
}
|
|
27
|
+
}
|
|
28
|
+
}, _jsx(SurfaceLayers, { material: material }), _jsx(SurfaceEdge, { material: material }), children);
|
|
21
29
|
});
|
|
30
|
+
export const Surface = SurfaceRoot;
|
|
31
|
+
function useResolvedSurface(color, options) {
|
|
32
|
+
const appearance = useAppearance();
|
|
33
|
+
const material = useMaterialOptions(options);
|
|
34
|
+
return {
|
|
35
|
+
...material,
|
|
36
|
+
color: useResolveColor(color),
|
|
37
|
+
foreground: useThemedValue(appearance.colors).foreground
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const layerStyle = {
|
|
41
|
+
position: "absolute",
|
|
42
|
+
inset: 0,
|
|
43
|
+
borderRadius: "inherit",
|
|
44
|
+
pointerEvents: "none"
|
|
45
|
+
};
|
|
46
|
+
function SurfaceLayers({ material }) {
|
|
47
|
+
const { backdrop, saturation, distortion } = material;
|
|
48
|
+
const frost = [
|
|
49
|
+
backdrop === 0 ? "" : `blur(${backdrop}px)`,
|
|
50
|
+
saturation === 1 ? "" : `saturate(${saturation})`
|
|
51
|
+
].filter(Boolean).join(" ");
|
|
52
|
+
const identity = `phresh-material-${useId().replaceAll(":", "")}`;
|
|
53
|
+
return _jsxs("span", { "data-material": "", "aria-hidden": "true", style: layerStyle, children: [_jsx(MotionStyle, {}), distortion > 0 && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), frost && _jsx(BackdropLayer, { name: "frost", filter: frost, zIndex: -2 }), _jsx(MaterialPaint, { identity: identity, distortion: distortion, color: material.color, grain: material.grain, grainAmount: material.grainAmount, opacity: material.opacity })] });
|
|
54
|
+
}
|
|
55
|
+
/** Keep refraction and native frost in independent compositor passes. */
|
|
56
|
+
function BackdropLayer({ filter, name, zIndex }) {
|
|
57
|
+
return _jsx("span", { "data-material-backdrop": name, "aria-hidden": "true", style: {
|
|
58
|
+
...layerStyle,
|
|
59
|
+
zIndex,
|
|
60
|
+
backdropFilter: filter,
|
|
61
|
+
WebkitBackdropFilter: filter
|
|
62
|
+
} });
|
|
63
|
+
}
|
package/dist/switch.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SwitchFieldProps } from "react-aria-components";
|
|
2
2
|
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
|
-
import type { MaterialOverrides } from "./material.js";
|
|
3
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
4
4
|
export interface SwitchProps extends Omit<SwitchFieldProps, ControlOverrides | "isReadOnly" | "isSelected" | "defaultSelected">, ControlProps, FieldProps, MaterialOverrides {
|
|
5
5
|
readonly checked?: boolean;
|
|
6
6
|
readonly defaultChecked?: boolean;
|
package/dist/text-control.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TextFieldProps } from "react-aria-components";
|
|
2
2
|
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
3
|
import type { RadiusProps } from "./radius.js";
|
|
4
|
-
import type { MaterialOverrides } from "./material.js";
|
|
4
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
5
5
|
/** Shared contract for native text-entry controls. */
|
|
6
6
|
export interface TextControlProps extends Omit<TextFieldProps, ControlOverrides | "isReadOnly">, ControlProps, FieldProps, RadiusProps, MaterialOverrides {
|
|
7
7
|
readonly placeholder?: string;
|
package/dist/textarea.js
CHANGED
|
@@ -2,12 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { TextArea as AriaTextarea, TextField } from "react-aria-components";
|
|
4
4
|
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SurfaceField } from "./control-surface.js";
|
|
6
6
|
import FieldStyle, { textControlClass } from "./field-style.js";
|
|
7
7
|
/** A labeled multiline text control, vertically resizable without escaping its width. */
|
|
8
8
|
export const Textarea = forwardRef(function Textarea({ label, description, errorMessage, disabled, readOnly, required, invalid, size, color, radius, style, className, placeholder, rows = 4, material, ...properties }, ref) {
|
|
9
9
|
const theme = useControlTheme({ size, color, radius });
|
|
10
|
-
return _jsxs(TextField, { ...properties, className: className, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldStyle, {}), _jsx(FieldLabel, { label: label }), _jsx(AriaTextarea, { ref: ref, placeholder: placeholder, rows: rows, className: textControlClass, render: (native, state) => _jsx(
|
|
10
|
+
return _jsxs(TextField, { ...properties, className: className, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldStyle, {}), _jsx(FieldLabel, { label: label }), _jsx(AriaTextarea, { ref: ref, placeholder: placeholder, rows: rows, className: textControlClass, render: (native, state) => _jsx(SurfaceField, { material: material, radius: theme.radius, paint: controlPaint(theme, state.isFocused, state.isInvalid, state.isHovered), children: _jsx("textarea", { ...native, style: { ...native.style, borderRadius: "inherit", background: "transparent" } }) }), style: state => ({
|
|
11
11
|
...controlStyle(theme, state.isFocused, state.isInvalid, state.isHovered, state.isFocusVisible),
|
|
12
12
|
height: "auto",
|
|
13
13
|
minHeight: theme.height,
|
package/dist/toggle-indicator.js
CHANGED
|
@@ -1,12 +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 {
|
|
5
|
-
import {
|
|
6
|
-
import { controlTransition, visualTransition } from "./motion-style.js";
|
|
4
|
+
import { useControlTransition } from "./motion-style.js";
|
|
5
|
+
import { Surface } from "./surface.js";
|
|
7
6
|
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
8
7
|
export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
|
|
9
8
|
const reduced = useReducedMotion();
|
|
9
|
+
const transition = useControlTransition(Boolean(reduced));
|
|
10
10
|
const { direction } = useLocale();
|
|
11
11
|
const diameter = theme.fontSize + 6;
|
|
12
12
|
const active = selected || indeterminate;
|
|
@@ -16,11 +16,9 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
|
|
|
16
16
|
const switching = kind === "switch";
|
|
17
17
|
const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
|
|
18
18
|
const travel = (diameter * 2 - thumbWidth) / 2 - 3;
|
|
19
|
-
|
|
20
|
-
return _jsxs(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: reduced || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: {
|
|
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: {
|
|
21
20
|
color: paint.color,
|
|
22
|
-
...
|
|
23
|
-
background: "transparent",
|
|
21
|
+
...theme.transition,
|
|
24
22
|
display: "inline-grid",
|
|
25
23
|
placeItems: "center",
|
|
26
24
|
position: "relative",
|
|
@@ -32,12 +30,12 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
|
|
|
32
30
|
border: "none",
|
|
33
31
|
outline: focused ? `2px solid ${theme.foreground}` : "none",
|
|
34
32
|
outlineOffset: 2,
|
|
35
|
-
}, children:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
35
|
+
height: diameter - 6, borderRadius: diameter,
|
|
36
|
+
background: paint.color
|
|
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 })] }) }) });
|
|
41
39
|
}
|
|
42
40
|
export function toggleStyle(theme, disabled, readOnly) {
|
|
43
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
|
}
|
package/dist/control-material.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { MaterialLayer, useResolvedMaterial } from "./material.js";
|
|
3
|
-
import { SurfaceEdge } from "./surface-edge.js";
|
|
4
|
-
/** A native button hosts Material without changing its native contract. */
|
|
5
|
-
export function MaterialButton({ native, paint, material: options }) {
|
|
6
|
-
const { ref, style, children, ...properties } = native;
|
|
7
|
-
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
8
|
-
return _jsxs("button", { ...properties, ref: ref, style: { ...style, background: "transparent", color: paint.color, position: style?.position ?? "relative", isolation: "isolate" }, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
|
|
9
|
-
}
|
|
10
|
-
/** Void text controls use a material host while retaining native sizing. */
|
|
11
|
-
export function MaterialField({ paint, radius, children, material: options }) {
|
|
12
|
-
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
13
|
-
return _jsxs("span", { style: { color: paint.color, display: "grid", minWidth: 0, borderRadius: radius, position: "relative", isolation: "isolate" }, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
|
|
14
|
-
}
|
package/dist/material.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type MaterialOptions } from "./material-options.js";
|
|
2
|
-
import { type Color } from "./color.js";
|
|
3
|
-
export type { MaterialOptions } from "./material-options.js";
|
|
4
|
-
/** Visual substance without host layout, content, interaction, or shadow. */
|
|
5
|
-
export interface MaterialProps {
|
|
6
|
-
readonly color?: Color;
|
|
7
|
-
readonly material?: MaterialOptions;
|
|
8
|
-
}
|
|
9
|
-
export interface MaterialOverrides {
|
|
10
|
-
readonly material?: MaterialOptions;
|
|
11
|
-
}
|
|
12
|
-
/** Fill the geometry of the nearest positioned host with the resolved material. */
|
|
13
|
-
export declare function Material(options?: MaterialProps): import("react").JSX.Element;
|
|
14
|
-
export declare function useResolvedMaterial({ color, material: options }?: MaterialProps): {
|
|
15
|
-
color: string;
|
|
16
|
-
foreground: string;
|
|
17
|
-
opacity: number;
|
|
18
|
-
backdrop: number;
|
|
19
|
-
grain: number;
|
|
20
|
-
grainAmount: number;
|
|
21
|
-
distortion: number;
|
|
22
|
-
saturation: number;
|
|
23
|
-
};
|
|
24
|
-
export type ResolvedMaterial = ReturnType<typeof useResolvedMaterial>;
|
|
25
|
-
export declare function MaterialLayer({ material }: Readonly<{
|
|
26
|
-
material: ResolvedMaterial;
|
|
27
|
-
}>): import("react").JSX.Element;
|
package/dist/material.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId } from "react";
|
|
3
|
-
import { useMaterialOptions } from "./material-options.js";
|
|
4
|
-
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
5
|
-
import { useResolveColor } from "./color.js";
|
|
6
|
-
import { MaterialPaint } from "./material-paint.js";
|
|
7
|
-
import MotionStyle from "./motion-style.js";
|
|
8
|
-
const layerStyle = {
|
|
9
|
-
position: "absolute",
|
|
10
|
-
inset: 0,
|
|
11
|
-
borderRadius: "inherit",
|
|
12
|
-
pointerEvents: "none"
|
|
13
|
-
};
|
|
14
|
-
/** Fill the geometry of the nearest positioned host with the resolved material. */
|
|
15
|
-
export function Material(options = {}) {
|
|
16
|
-
const material = useResolvedMaterial(options);
|
|
17
|
-
return _jsx(MaterialLayer, { material: material });
|
|
18
|
-
}
|
|
19
|
-
export function useResolvedMaterial({ color, material: options } = {}) {
|
|
20
|
-
const appearance = useAppearance();
|
|
21
|
-
const material = useMaterialOptions(options);
|
|
22
|
-
return {
|
|
23
|
-
...material,
|
|
24
|
-
color: useResolveColor(color),
|
|
25
|
-
foreground: useResolveTheme(appearance.colors.foreground)
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export function MaterialLayer({ material }) {
|
|
29
|
-
const { backdrop, saturation, distortion } = material;
|
|
30
|
-
const frost = [
|
|
31
|
-
backdrop === 0 ? "" : `blur(${backdrop}px)`,
|
|
32
|
-
saturation === 1 ? "" : `saturate(${saturation})`
|
|
33
|
-
].filter(Boolean).join(" ");
|
|
34
|
-
const identity = `phresh-material-${useId().replaceAll(":", "")}`;
|
|
35
|
-
return _jsxs("span", { "data-material": "", "aria-hidden": "true", style: layerStyle, children: [_jsx(MotionStyle, {}), distortion > 0 && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), frost && _jsx(BackdropLayer, { name: "frost", filter: frost, zIndex: -2 }), _jsx(MaterialPaint, { identity: identity, distortion: distortion, color: material.color, grain: material.grain, grainAmount: material.grainAmount, opacity: material.opacity })] });
|
|
36
|
-
}
|
|
37
|
-
/** Keep refraction and native frost in independent compositor passes. */
|
|
38
|
-
function BackdropLayer({ filter, name, zIndex }) {
|
|
39
|
-
return _jsx("span", { "data-material-backdrop": name, "aria-hidden": "true", style: {
|
|
40
|
-
...layerStyle,
|
|
41
|
-
zIndex,
|
|
42
|
-
backdropFilter: filter,
|
|
43
|
-
WebkitBackdropFilter: filter
|
|
44
|
-
} });
|
|
45
|
-
}
|