@phreshos/react-ui 0.1.16 → 0.1.17
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 +164 -8
- package/dist/appearance-provider.d.ts +6 -8
- package/dist/appearance-provider.js +34 -19
- package/dist/button.d.ts +11 -5
- package/dist/button.js +28 -36
- package/dist/checkbox.d.ts +11 -0
- package/dist/checkbox.js +10 -0
- package/dist/color.d.ts +33 -0
- package/dist/color.js +84 -5
- package/dist/control-material.d.ts +20 -0
- package/dist/control-material.js +14 -0
- package/dist/control.d.ts +55 -0
- package/dist/control.js +87 -0
- package/dist/document-scrollbars.js +1 -1
- package/dist/field-style.d.ts +3 -0
- package/dist/field-style.js +11 -0
- package/dist/flex.js +2 -2
- package/dist/grid.js +2 -2
- package/dist/input.d.ts +4 -0
- package/dist/input.js +11 -0
- package/dist/main.d.ts +13 -2
- package/dist/main.js +10 -0
- package/dist/material-options.d.ts +20 -0
- package/dist/material-options.js +22 -0
- package/dist/material-paint.d.ts +11 -0
- package/dist/material-paint.js +76 -0
- package/dist/material.d.ts +27 -0
- package/dist/material.js +45 -0
- package/dist/motion-style.d.ts +21 -0
- package/dist/motion-style.js +45 -0
- package/dist/panel.d.ts +1 -1
- package/dist/radio.d.ts +14 -0
- package/dist/radio.js +17 -0
- package/dist/select.d.ts +17 -0
- package/dist/select.js +28 -0
- package/dist/slider.d.ts +7 -0
- package/dist/slider.js +36 -0
- package/dist/surface-edge.d.ts +7 -0
- package/dist/surface-edge.js +59 -0
- package/dist/surface.d.ts +7 -44
- package/dist/surface.js +18 -98
- package/dist/switch.d.ts +10 -0
- package/dist/switch.js +10 -0
- package/dist/text-control.d.ts +9 -0
- package/dist/text-control.js +0 -0
- package/dist/textarea.d.ts +6 -0
- package/dist/textarea.js +17 -0
- package/dist/toggle-indicator.d.ts +16 -0
- package/dist/toggle-indicator.js +51 -0
- package/package.json +3 -3
- package/dist/surface-material.d.ts +0 -14
- package/dist/surface-material.js +0 -128
package/dist/select.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import { Select as AriaSelect, Button, SelectValue, Popover, ListBox, ListBoxItem } from "react-aria-components";
|
|
5
|
+
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
6
|
+
import { MaterialButton } from "./control-material.js";
|
|
7
|
+
import { Surface } from "./surface.js";
|
|
8
|
+
import { controlTransition, overlayMotionClass, visualTransition } from "./motion-style.js";
|
|
9
|
+
/** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
|
|
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
|
+
const theme = useControlTheme({ size, color, radius });
|
|
12
|
+
const reduced = useReducedMotion();
|
|
13
|
+
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(MaterialButton, { native: native, material: material, paint: controlPaint(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered) }), style: button => ({
|
|
14
|
+
...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
|
|
15
|
+
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
16
|
+
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: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, 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: { 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 => ({
|
|
18
|
+
...visualTransition,
|
|
19
|
+
display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
|
|
20
|
+
minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
|
|
21
|
+
borderRadius: theme.radius,
|
|
22
|
+
outline: item.isFocused && !item.isSelected ? `2px solid ${theme.foreground}` : "none", outlineOffset: -2,
|
|
23
|
+
cursor: item.isDisabled ? "not-allowed" : "pointer",
|
|
24
|
+
opacity: item.isDisabled ? 0.46 : 1,
|
|
25
|
+
...(item.isSelected ? item.isFocused ? theme.paints.palette.hover : theme.paints.palette.rest
|
|
26
|
+
: { background: "transparent", color: theme.foreground })
|
|
27
|
+
}), children: item => _jsxs(_Fragment, { children: [option.label, _jsx("span", { "aria-hidden": "true", children: item.isSelected ? "✓" : null })] }) }) }) }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] }) });
|
|
28
|
+
});
|
package/dist/slider.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SliderProps as AriaSliderProps } from "react-aria-components";
|
|
2
|
+
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
|
+
export interface SliderProps extends Omit<AriaSliderProps<number>, ControlOverrides>, ControlProps, Pick<FieldProps, "label" | "description"> {
|
|
4
|
+
readonly name?: string;
|
|
5
|
+
}
|
|
6
|
+
/** One numeric value with pointer, touch, keyboard, and form support. */
|
|
7
|
+
export declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/slider.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import { Slider as AriaSlider, SliderTrack, SliderThumb, SliderFill, SliderOutput } from "react-aria-components";
|
|
5
|
+
import { FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
6
|
+
import { controlTransition, visualTransition } from "./motion-style.js";
|
|
7
|
+
/** One numeric value with pointer, touch, keyboard, and form support. */
|
|
8
|
+
export const Slider = forwardRef(function Slider({ label, description, name, disabled, size, color, style, orientation = "horizontal", ...properties }, ref) {
|
|
9
|
+
const theme = useControlTheme({ size, color });
|
|
10
|
+
const descriptionId = useId();
|
|
11
|
+
const vertical = orientation === "vertical";
|
|
12
|
+
const diameter = theme.fontSize + 6;
|
|
13
|
+
const reduced = useReducedMotion();
|
|
14
|
+
const rail = Math.max(4, Math.round(diameter / 3));
|
|
15
|
+
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
|
+
position: "relative", touchAction: "none",
|
|
17
|
+
width: vertical ? theme.height : "100%", height: vertical ? 160 : theme.height
|
|
18
|
+
}, children: [_jsx("div", { "aria-hidden": "true", style: {
|
|
19
|
+
...visualTransition,
|
|
20
|
+
position: "absolute", borderRadius: rail,
|
|
21
|
+
...(vertical ? { width: rail, height: "100%", left: "50%", transform: "translateX(-50%)" } : { height: rail, width: "100%", top: "50%", transform: "translateY(-50%)" }),
|
|
22
|
+
background: theme.paints.neutral.rest.background
|
|
23
|
+
} }), _jsx(SliderFill, { style: {
|
|
24
|
+
...visualTransition,
|
|
25
|
+
position: "absolute", background: theme.paints.palette.rest.background, borderRadius: rail,
|
|
26
|
+
...(vertical ? { width: rail, left: "50%", transform: "translateX(-50%)" } : { height: rail, top: "50%", transform: "translateY(-50%)" })
|
|
27
|
+
} }), _jsx(SliderThumb, { name: name, style: state => ({
|
|
28
|
+
width: diameter, height: diameter, borderRadius: "50%", boxSizing: "border-box",
|
|
29
|
+
...(vertical ? { left: "50%" } : { top: "50%" }),
|
|
30
|
+
outline: state.isFocusVisible ? `2px solid ${theme.foreground}` : "none", outlineOffset: 2,
|
|
31
|
+
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: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: { ...visualTransition, position: "absolute", inset: 0, borderRadius: "inherit", pointerEvents: "none",
|
|
33
|
+
background: (state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).color,
|
|
34
|
+
border: `3px solid ${(state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).background}`,
|
|
35
|
+
boxSizing: "border-box" } }) })] }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
|
|
36
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedMaterial } from "./material.js";
|
|
2
|
+
type EdgeMaterial = Pick<ResolvedMaterial, "color" | "foreground" | "opacity">;
|
|
3
|
+
/** Paints the boundary owned by one Surface from the material it contains. */
|
|
4
|
+
export declare function SurfaceEdge({ material }: Readonly<{
|
|
5
|
+
material: EdgeMaterial;
|
|
6
|
+
}>): import("react").JSX.Element | null;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { colorLightness, colorOpacity, orderColors } from "./color.js";
|
|
4
|
+
import { paintTransition } from "./motion-style.js";
|
|
5
|
+
import { scale } from "./scale.js";
|
|
6
|
+
/** Paints the boundary owned by one Surface from the material it contains. */
|
|
7
|
+
export function SurfaceEdge({ material }) {
|
|
8
|
+
const visible = material.opacity > 0;
|
|
9
|
+
const palette = useRef(null);
|
|
10
|
+
const [colors, setColors] = useState(null);
|
|
11
|
+
const updatePalette = useCallback(() => {
|
|
12
|
+
const base = palette.current;
|
|
13
|
+
const view = base?.ownerDocument.defaultView;
|
|
14
|
+
if (!base || !view)
|
|
15
|
+
return;
|
|
16
|
+
const computed = view.getComputedStyle(base);
|
|
17
|
+
const background = computed.fill;
|
|
18
|
+
const foreground = computed.color;
|
|
19
|
+
setColors(previous => previous?.background === background && previous.foreground === foreground ? previous : {
|
|
20
|
+
...orderColors(background, foreground),
|
|
21
|
+
background,
|
|
22
|
+
foreground,
|
|
23
|
+
lightness: Math.max(0, Math.min(1, colorLightness(background)))
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
useLayoutEffect(updatePalette);
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
const base = palette.current;
|
|
29
|
+
if (!base)
|
|
30
|
+
return;
|
|
31
|
+
const complete = (event) => {
|
|
32
|
+
if (event.target === base && (event.propertyName === "fill" || event.propertyName === "color"))
|
|
33
|
+
updatePalette();
|
|
34
|
+
};
|
|
35
|
+
base.addEventListener("transitionend", complete);
|
|
36
|
+
return () => base.removeEventListener("transitionend", complete);
|
|
37
|
+
}, [updatePalette, visible]);
|
|
38
|
+
if (!visible)
|
|
39
|
+
return null;
|
|
40
|
+
return _jsxs(Fragment, { children: [_jsx("svg", { "data-surface-palette": "", "aria-hidden": "true", width: "0", height: "0", style: { position: "absolute", pointerEvents: "none" }, children: _jsx("rect", { ref: palette, "data-surface-palette-base": "", style: { ...paintTransition, fill: material.color, color: material.foreground } }) }), _jsx("span", { "data-surface-edge": "", "aria-hidden": "true", style: {
|
|
41
|
+
...paintTransition,
|
|
42
|
+
position: "absolute",
|
|
43
|
+
zIndex: 1,
|
|
44
|
+
inset: 0,
|
|
45
|
+
padding: 1,
|
|
46
|
+
borderRadius: "inherit",
|
|
47
|
+
pointerEvents: "none",
|
|
48
|
+
opacity: Math.min(1, scale(material.opacity, "xlarge")) * (colors?.lightness ?? 0),
|
|
49
|
+
background: colors ? glassEdge(colors.lighter, colors.darker) : undefined,
|
|
50
|
+
WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
|
|
51
|
+
WebkitMaskComposite: "xor",
|
|
52
|
+
mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
|
|
53
|
+
maskComposite: "exclude"
|
|
54
|
+
} })] });
|
|
55
|
+
}
|
|
56
|
+
function glassEdge(light, dark) {
|
|
57
|
+
const edge = `color-mix(in oklch, ${dark} 20%, ${light})`;
|
|
58
|
+
return `linear-gradient(145deg, ${colorOpacity(light, 0.92)}, ${colorOpacity(light, 0.4)} 35%, ${colorOpacity(edge, 0.18)} 55%, ${colorOpacity(light, 0.6)} 85%, ${colorOpacity(light, 0.3)})`;
|
|
59
|
+
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -1,44 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
grainAmount?: ScaleLevel | number;
|
|
9
|
-
/** Appearance-derived level or direct backdrop blur from zero to 24 CSS pixels. */
|
|
10
|
-
backdrop?: ScaleLevel | number;
|
|
11
|
-
/** Appearance-derived level or direct material opacity from zero to one. */
|
|
12
|
-
opacity?: ScaleLevel | number;
|
|
13
|
-
/** Appearance-derived level or direct organic displacement from zero to 140 pixels. */
|
|
14
|
-
distortion?: ScaleLevel | number;
|
|
15
|
-
/** Appearance-derived level or direct directional displacement from zero to 40 pixels. */
|
|
16
|
-
waves?: ScaleLevel | number;
|
|
17
|
-
/** Appearance-derived level or direct ripple displacement from zero to 40 pixels. */
|
|
18
|
-
ripples?: ScaleLevel | number;
|
|
19
|
-
/** Appearance-derived level or direct backdrop saturation multiplier. */
|
|
20
|
-
saturation?: ScaleLevel | number;
|
|
21
|
-
/** Appearance-derived level or direct backdrop brightness multiplier. */
|
|
22
|
-
brightness?: ScaleLevel | number;
|
|
23
|
-
}>;
|
|
24
|
-
/** Contains content above locally owned Surface material layers. */
|
|
25
|
-
export declare const Surface: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "opacity"> & Readonly<{
|
|
26
|
-
/** Appearance-derived level or direct grain intensity from zero to one. */
|
|
27
|
-
grain?: ScaleLevel | number;
|
|
28
|
-
/** Appearance-derived level or direct retained grain amount from zero to one. */
|
|
29
|
-
grainAmount?: ScaleLevel | number;
|
|
30
|
-
/** Appearance-derived level or direct backdrop blur from zero to 24 CSS pixels. */
|
|
31
|
-
backdrop?: ScaleLevel | number;
|
|
32
|
-
/** Appearance-derived level or direct material opacity from zero to one. */
|
|
33
|
-
opacity?: ScaleLevel | number;
|
|
34
|
-
/** Appearance-derived level or direct organic displacement from zero to 140 pixels. */
|
|
35
|
-
distortion?: ScaleLevel | number;
|
|
36
|
-
/** Appearance-derived level or direct directional displacement from zero to 40 pixels. */
|
|
37
|
-
waves?: ScaleLevel | number;
|
|
38
|
-
/** Appearance-derived level or direct ripple displacement from zero to 40 pixels. */
|
|
39
|
-
ripples?: ScaleLevel | number;
|
|
40
|
-
/** Appearance-derived level or direct backdrop saturation multiplier. */
|
|
41
|
-
saturation?: ScaleLevel | number;
|
|
42
|
-
/** Appearance-derived level or direct backdrop brightness multiplier. */
|
|
43
|
-
brightness?: ScaleLevel | number;
|
|
44
|
-
}> & import("react").RefAttributes<HTMLDivElement>>;
|
|
1
|
+
import type { ComponentPropsWithRef } from "react";
|
|
2
|
+
import { type MaterialProps } from "./material.js";
|
|
3
|
+
import { type RadiusProps } from "./radius.js";
|
|
4
|
+
/** A div that hosts one Material and owns its geometry. */
|
|
5
|
+
export interface SurfaceProps extends Omit<ComponentPropsWithRef<"div">, "color">, MaterialProps, RadiusProps {
|
|
6
|
+
}
|
|
7
|
+
export declare const Surface: import("react").ForwardRefExoticComponent<Omit<SurfaceProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/surface.js
CHANGED
|
@@ -1,101 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
inset: 0,
|
|
10
|
-
borderRadius: "inherit",
|
|
11
|
-
pointerEvents: "none"
|
|
12
|
-
};
|
|
13
|
-
/** Contains content above locally owned Surface material layers. */
|
|
14
|
-
export const Surface = forwardRef(function Surface({ backdrop, brightness, children, distortion, grain, grainAmount, opacity, ripples, saturation, style, waves, ...properties }, forwardedRef) {
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { useAppearance } from "./appearance-provider.js";
|
|
4
|
+
import { MaterialLayer, useResolvedMaterial } from "./material.js";
|
|
5
|
+
import { visualTransition } from "./motion-style.js";
|
|
6
|
+
import { resolveRadius } from "./radius.js";
|
|
7
|
+
import { SurfaceEdge } from "./surface-edge.js";
|
|
8
|
+
export const Surface = forwardRef(function Surface({ color, material: options, radius, children, style, ...properties }, ref) {
|
|
15
9
|
const appearance = useAppearance();
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
forwardedRef.current = node;
|
|
28
|
-
}, [forwardedRef]);
|
|
29
|
-
const resolved = resolveSurface({ backdrop, brightness, distortion, grain, grainAmount, opacity, ripples, saturation, waves }, background, surface);
|
|
30
|
-
useLayoutEffect(() => {
|
|
31
|
-
const surface = element.current;
|
|
32
|
-
if (surface)
|
|
33
|
-
return prepareSurfaceLayout(surface);
|
|
34
|
-
});
|
|
35
|
-
return _jsxs("div", { ...properties, ref: capture, style: {
|
|
36
|
-
borderRadius: radius,
|
|
37
|
-
color: foreground,
|
|
38
|
-
...style
|
|
39
|
-
}, children: [resolved.refracts && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), resolved.frost && _jsx(BackdropLayer, { name: "frost", filter: resolved.frost, zIndex: -2 }), _jsx(SurfaceMaterial, { identity: identity, foreground: foreground, ...resolved.material }), children] });
|
|
10
|
+
const material = useResolvedMaterial({ color, material: options });
|
|
11
|
+
const resolvedRadius = resolveRadius(radius ?? "medium", appearance);
|
|
12
|
+
return _jsxs("div", { ...properties, ref: ref, style: {
|
|
13
|
+
...visualTransition,
|
|
14
|
+
background: "transparent",
|
|
15
|
+
color: material.foreground,
|
|
16
|
+
...style,
|
|
17
|
+
borderRadius: radius === undefined ? style?.borderRadius ?? resolvedRadius : resolvedRadius,
|
|
18
|
+
position: style?.position ?? "relative",
|
|
19
|
+
isolation: "isolate"
|
|
20
|
+
}, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
|
|
40
21
|
});
|
|
41
|
-
/** Keeps refraction and native frost in independent compositor passes. */
|
|
42
|
-
function BackdropLayer({ filter, name, zIndex }) {
|
|
43
|
-
return _jsx("div", { "data-surface-backdrop": name, "aria-hidden": "true", style: {
|
|
44
|
-
...layerStyle,
|
|
45
|
-
zIndex,
|
|
46
|
-
backdropFilter: filter,
|
|
47
|
-
WebkitBackdropFilter: filter
|
|
48
|
-
} });
|
|
49
|
-
}
|
|
50
|
-
function resolveSurface(values, background, surface) {
|
|
51
|
-
const material = {
|
|
52
|
-
color: background,
|
|
53
|
-
distortion: resolveScale(values.distortion, surface.distortion, appearanceLimits.surface.distortion),
|
|
54
|
-
grain: resolveScale(values.grain, surface.grain, appearanceLimits.surface.grain),
|
|
55
|
-
grainAmount: resolveScale(values.grainAmount, surface.grainAmount, appearanceLimits.surface.grainAmount),
|
|
56
|
-
opacity: resolveScale(values.opacity, surface.opacity, appearanceLimits.surface.opacity),
|
|
57
|
-
ripples: resolveScale(values.ripples, surface.ripples, appearanceLimits.surface.ripples),
|
|
58
|
-
waves: resolveScale(values.waves, surface.waves, appearanceLimits.surface.waves)
|
|
59
|
-
};
|
|
60
|
-
const backdrop = resolveScale(values.backdrop, surface.backdrop, appearanceLimits.surface.backdrop);
|
|
61
|
-
const saturation = resolveMultiplier(values.saturation, surface.saturation, appearanceLimits.surface.saturation);
|
|
62
|
-
const brightness = resolveMultiplier(values.brightness, surface.brightness, appearanceLimits.surface.brightness);
|
|
63
|
-
const frost = [
|
|
64
|
-
backdrop === 0 ? "" : `blur(${backdrop}px)`,
|
|
65
|
-
saturation === 1 ? "" : `saturate(${saturation})`,
|
|
66
|
-
brightness === 1 ? "" : `brightness(${brightness})`
|
|
67
|
-
].filter(Boolean).join(" ");
|
|
68
|
-
return {
|
|
69
|
-
material,
|
|
70
|
-
frost,
|
|
71
|
-
refracts: material.distortion > 0 || material.waves > 0 || material.ripples > 0
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function resolveScale(value, base, range) {
|
|
75
|
-
const resolved = isScaleLevel(value) ? scale(base, value) : value ?? base;
|
|
76
|
-
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
77
|
-
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
78
|
-
}
|
|
79
|
-
function resolveMultiplier(value, base, range) {
|
|
80
|
-
const resolved = isScaleLevel(value) ? scaleMultiplier(base, value) : value ?? base;
|
|
81
|
-
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
82
|
-
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
83
|
-
}
|
|
84
|
-
function prepareSurfaceLayout(element) {
|
|
85
|
-
const view = element.ownerDocument.defaultView;
|
|
86
|
-
const computed = view?.getComputedStyle(element);
|
|
87
|
-
const position = element.style.position;
|
|
88
|
-
const isolation = element.style.isolation;
|
|
89
|
-
const ownsPosition = computed?.position === "static";
|
|
90
|
-
const ownsIsolation = computed?.isolation !== "isolate";
|
|
91
|
-
if (ownsPosition)
|
|
92
|
-
element.style.position = "relative";
|
|
93
|
-
if (ownsIsolation)
|
|
94
|
-
element.style.isolation = "isolate";
|
|
95
|
-
return () => {
|
|
96
|
-
if (ownsPosition && element.style.position === "relative")
|
|
97
|
-
element.style.position = position;
|
|
98
|
-
if (ownsIsolation && element.style.isolation === "isolate")
|
|
99
|
-
element.style.isolation = isolation;
|
|
100
|
-
};
|
|
101
|
-
}
|
package/dist/switch.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SwitchFieldProps } from "react-aria-components";
|
|
2
|
+
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
|
+
import type { MaterialOverrides } from "./material.js";
|
|
4
|
+
export interface SwitchProps extends Omit<SwitchFieldProps, ControlOverrides | "isReadOnly" | "isSelected" | "defaultSelected">, ControlProps, FieldProps, MaterialOverrides {
|
|
5
|
+
readonly checked?: boolean;
|
|
6
|
+
readonly defaultChecked?: boolean;
|
|
7
|
+
readonly readOnly?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** An on/off setting. onChange receives the next boolean value. */
|
|
10
|
+
export declare const Switch: import("react").ForwardRefExoticComponent<SwitchProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/switch.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { SwitchField, SwitchButton } from "react-aria-components";
|
|
4
|
+
import { FieldFeedback, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
+
import { ToggleIndicator, toggleStyle } from "./toggle-indicator.js";
|
|
6
|
+
/** An on/off setting. onChange receives the next boolean value. */
|
|
7
|
+
export const Switch = forwardRef(function Switch({ label, description, errorMessage, disabled, required, invalid, readOnly, checked, defaultChecked, size, color, style, material, ...properties }, ref) {
|
|
8
|
+
const theme = useControlTheme({ size, color });
|
|
9
|
+
return _jsxs(SwitchField, { ...properties, ref: ref, isDisabled: disabled, isRequired: required, isInvalid: invalid, isReadOnly: readOnly, isSelected: checked, defaultSelected: defaultChecked, style: state => fieldStyle(theme, state.isDisabled, style), children: [_jsx(SwitchButton, { style: state => toggleStyle(theme, state.isDisabled, state.isReadOnly), children: state => _jsxs(_Fragment, { children: [_jsx(ToggleIndicator, { kind: "switch", material: material, theme: theme, selected: state.isSelected, focused: state.isFocusVisible, invalid: state.isInvalid, hovered: !state.isDisabled && !state.isReadOnly && state.isHovered, pressed: !state.isDisabled && !state.isReadOnly && state.isPressed }), label] }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TextFieldProps } from "react-aria-components";
|
|
2
|
+
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
3
|
+
import type { RadiusProps } from "./radius.js";
|
|
4
|
+
import type { MaterialOverrides } from "./material.js";
|
|
5
|
+
/** Shared contract for native text-entry controls. */
|
|
6
|
+
export interface TextControlProps extends Omit<TextFieldProps, ControlOverrides | "isReadOnly">, ControlProps, FieldProps, RadiusProps, MaterialOverrides {
|
|
7
|
+
readonly placeholder?: string;
|
|
8
|
+
readonly readOnly?: boolean;
|
|
9
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TextControlProps } from "./text-control.js";
|
|
2
|
+
export interface TextareaProps extends Omit<TextControlProps, "type" | "pattern"> {
|
|
3
|
+
readonly rows?: number;
|
|
4
|
+
}
|
|
5
|
+
/** A labeled multiline text control, vertically resizable without escaping its width. */
|
|
6
|
+
export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
package/dist/textarea.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { TextArea as AriaTextarea, TextField } from "react-aria-components";
|
|
4
|
+
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
+
import { MaterialField } from "./control-material.js";
|
|
6
|
+
import FieldStyle, { textControlClass } from "./field-style.js";
|
|
7
|
+
/** A labeled multiline text control, vertically resizable without escaping its width. */
|
|
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
|
+
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(MaterialField, { 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
|
+
...controlStyle(theme, state.isFocused, state.isInvalid, state.isHovered, state.isFocusVisible),
|
|
12
|
+
height: "auto",
|
|
13
|
+
minHeight: theme.height,
|
|
14
|
+
paddingBlock: Math.max(6, theme.spacing / 2),
|
|
15
|
+
resize: "vertical"
|
|
16
|
+
}) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { ControlTheme } from "./control.js";
|
|
3
|
+
import type { MaterialOptions } from "./material-options.js";
|
|
4
|
+
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
5
|
+
export declare function ToggleIndicator({ kind, selected, indeterminate, focused, invalid, hovered, pressed, theme, material: options }: Readonly<{
|
|
6
|
+
kind: "checkbox" | "radio" | "switch";
|
|
7
|
+
selected: boolean;
|
|
8
|
+
indeterminate?: boolean;
|
|
9
|
+
focused: boolean;
|
|
10
|
+
invalid?: boolean;
|
|
11
|
+
hovered?: boolean;
|
|
12
|
+
pressed?: boolean;
|
|
13
|
+
theme: ControlTheme;
|
|
14
|
+
material?: MaterialOptions;
|
|
15
|
+
}>): import("react").JSX.Element;
|
|
16
|
+
export declare function toggleStyle(theme: ControlTheme, disabled: boolean, readOnly: boolean): CSSProperties;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
3
|
+
import { useLocale } from "react-aria-components";
|
|
4
|
+
import { MaterialLayer, useResolvedMaterial } from "./material.js";
|
|
5
|
+
import { SurfaceEdge } from "./surface-edge.js";
|
|
6
|
+
import { controlTransition, visualTransition } from "./motion-style.js";
|
|
7
|
+
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
8
|
+
export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
|
|
9
|
+
const reduced = useReducedMotion();
|
|
10
|
+
const { direction } = useLocale();
|
|
11
|
+
const diameter = theme.fontSize + 6;
|
|
12
|
+
const active = selected || indeterminate;
|
|
13
|
+
const paints = invalid ? theme.paints.danger : active ? theme.paints.palette : theme.paints.neutral;
|
|
14
|
+
const paint = pressed ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
15
|
+
const radius = kind === "checkbox" ? Math.min(diameter / 4, typeof theme.radius === "number" ? theme.radius : diameter / 4) : diameter;
|
|
16
|
+
const switching = kind === "switch";
|
|
17
|
+
const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
|
|
18
|
+
const travel = (diameter * 2 - thumbWidth) / 2 - 3;
|
|
19
|
+
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
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: {
|
|
21
|
+
color: paint.color,
|
|
22
|
+
...visualTransition,
|
|
23
|
+
background: "transparent",
|
|
24
|
+
display: "inline-grid",
|
|
25
|
+
placeItems: "center",
|
|
26
|
+
position: "relative",
|
|
27
|
+
boxSizing: "border-box",
|
|
28
|
+
flexShrink: 0,
|
|
29
|
+
width: switching ? diameter * 2 : diameter,
|
|
30
|
+
height: diameter,
|
|
31
|
+
borderRadius: radius,
|
|
32
|
+
border: "none",
|
|
33
|
+
outline: focused ? `2px solid ${theme.foreground}` : "none",
|
|
34
|
+
outlineOffset: 2,
|
|
35
|
+
}, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), switching ? _jsx(motion.span, { initial: false, animate: { width: thumbWidth, x: (selected ? travel : -travel) * (direction === "rtl" ? -1 : 1) }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: {
|
|
36
|
+
...visualTransition,
|
|
37
|
+
height: diameter - 6, borderRadius: diameter,
|
|
38
|
+
background: paint.color
|
|
39
|
+
} }) : _jsx(motion.span, { initial: false, animate: { opacity: active ? 1 : 0, scale: active ? 1 : 0.7 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: { display: "grid", placeItems: "center" }, children: kind === "radio" ? _jsx("span", { style: { ...visualTransition, width: diameter / 2, height: diameter / 2, borderRadius: "50%", background: "currentColor" } })
|
|
40
|
+
: _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: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration } }), _jsx(motion.path, { d: "M4 8h8", initial: false, animate: { pathLength: indeterminate ? 1 : 0, opacity: indeterminate ? 1 : 0 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration } })] }) })] });
|
|
41
|
+
}
|
|
42
|
+
export function toggleStyle(theme, disabled, readOnly) {
|
|
43
|
+
return {
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
gap: theme.gap,
|
|
47
|
+
minHeight: theme.height,
|
|
48
|
+
width: "fit-content",
|
|
49
|
+
cursor: disabled ? "not-allowed" : readOnly ? "default" : "pointer"
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"prepack": "node --run test && node --run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@phreshos/core": "^0.1.41",
|
|
56
55
|
"react": "^19.2.0",
|
|
57
56
|
"react-dom": "^19.2.0"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
59
|
+
"@phreshos/core": "^0.1.42",
|
|
60
60
|
"colorjs.io": "^0.7.1",
|
|
61
|
+
"motion": "^13.2.0",
|
|
61
62
|
"react-aria-components": "^1.20.0"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@base-ui/react": "^1.7.0",
|
|
65
|
-
"@phreshos/core": "^0.1.41",
|
|
66
66
|
"@testing-library/dom": "^10.4.1",
|
|
67
67
|
"@testing-library/react": "^16.3.0",
|
|
68
68
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
interface SurfaceMaterialProps {
|
|
2
|
-
readonly color: string;
|
|
3
|
-
readonly distortion: number;
|
|
4
|
-
readonly foreground: string;
|
|
5
|
-
readonly grain: number;
|
|
6
|
-
readonly grainAmount: number;
|
|
7
|
-
readonly identity: string;
|
|
8
|
-
readonly opacity: number;
|
|
9
|
-
readonly ripples: number;
|
|
10
|
-
readonly waves: number;
|
|
11
|
-
}
|
|
12
|
-
/** The locally owned fill, refraction definition, and glass rim inside one Surface. */
|
|
13
|
-
export declare function SurfaceMaterial({ color, distortion, foreground, grain, grainAmount, identity, opacity, ripples, waves }: SurfaceMaterialProps): import("react").JSX.Element | null;
|
|
14
|
-
export {};
|