@phreshos/react-ui 0.1.16 → 0.1.18
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 +158 -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-surface.d.ts +20 -0
- package/dist/control-surface.js +11 -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 -3
- package/dist/main.js +9 -0
- package/dist/material-options.d.ts +24 -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/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 +10 -0
- package/dist/surface-edge.js +59 -0
- package/dist/surface.d.ts +23 -44
- package/dist/surface.js +47 -86
- 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 +48 -0
- package/package.json +3 -3
- package/dist/surface-material.d.ts +0 -14
- package/dist/surface-material.js +0 -128
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type RadiusProps } from "./radius.js";
|
|
3
|
+
import { type ScaleLevel } from "./scale.js";
|
|
4
|
+
import { solidColors } from "./color.js";
|
|
5
|
+
import { type Color } from "./color.js";
|
|
6
|
+
/** Semantic Appearance colors accepted by interactive controls. */
|
|
7
|
+
export type ControlColor = Color;
|
|
8
|
+
export interface ControlProps {
|
|
9
|
+
readonly size?: ScaleLevel;
|
|
10
|
+
readonly color?: ControlColor;
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
readonly style?: CSSProperties;
|
|
13
|
+
readonly disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FieldProps {
|
|
16
|
+
/** Visible accessible label. Otherwise supply aria-label or aria-labelledby. */
|
|
17
|
+
readonly label?: ReactNode;
|
|
18
|
+
readonly description?: ReactNode;
|
|
19
|
+
readonly errorMessage?: ReactNode;
|
|
20
|
+
readonly required?: boolean;
|
|
21
|
+
readonly invalid?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Names owned by our contract, not a second set of primitive-specific aliases. */
|
|
24
|
+
export type ControlOverrides = keyof ControlProps | keyof FieldProps | "children" | "isDisabled" | "isRequired" | "isInvalid";
|
|
25
|
+
export declare const controlFontSizes: Readonly<Record<ScaleLevel, number>>;
|
|
26
|
+
type SolidColors = ReturnType<typeof solidColors>;
|
|
27
|
+
export interface ControlTheme {
|
|
28
|
+
readonly spacing: number;
|
|
29
|
+
readonly foreground: string;
|
|
30
|
+
readonly background: string;
|
|
31
|
+
readonly tint: string;
|
|
32
|
+
readonly colored: boolean;
|
|
33
|
+
readonly danger: string;
|
|
34
|
+
readonly paints: {
|
|
35
|
+
readonly palette: SolidColors;
|
|
36
|
+
readonly neutral: SolidColors;
|
|
37
|
+
readonly danger: SolidColors;
|
|
38
|
+
};
|
|
39
|
+
readonly radius: CSSProperties["borderRadius"];
|
|
40
|
+
readonly fontSize: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
readonly gap: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function useControlTheme({ size, color, radius }: ControlProps & RadiusProps): ControlTheme;
|
|
45
|
+
export declare function fieldStyle(theme: ControlTheme, disabled?: boolean, style?: CSSProperties): CSSProperties;
|
|
46
|
+
export declare function controlPaint(theme: ControlTheme, focused: boolean, invalid: boolean, hovered?: boolean): {
|
|
47
|
+
background: string;
|
|
48
|
+
color: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function controlStyle(theme: ControlTheme, focused: boolean, invalid: boolean, hovered?: boolean, focusVisible?: boolean): CSSProperties;
|
|
51
|
+
export declare function FieldLabel({ label }: Pick<FieldProps, "label">): import("react").JSX.Element | null;
|
|
52
|
+
export declare function FieldFeedback({ description, errorMessage, theme }: Pick<FieldProps, "description" | "errorMessage"> & {
|
|
53
|
+
readonly theme: ControlTheme;
|
|
54
|
+
}): import("react").JSX.Element;
|
|
55
|
+
export {};
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { FieldError, Label, Text } from "react-aria-components";
|
|
4
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
5
|
+
import { resolveRadius } from "./radius.js";
|
|
6
|
+
import { scale } from "./scale.js";
|
|
7
|
+
import { solidColors } from "./color.js";
|
|
8
|
+
import { useResolveSolidColor } from "./color.js";
|
|
9
|
+
import { visualTransition } from "./motion-style.js";
|
|
10
|
+
export const controlFontSizes = Object.freeze({
|
|
11
|
+
xsmall: 11,
|
|
12
|
+
small: 12,
|
|
13
|
+
medium: 13,
|
|
14
|
+
large: 14,
|
|
15
|
+
xlarge: 15
|
|
16
|
+
});
|
|
17
|
+
export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
18
|
+
const appearance = useAppearance();
|
|
19
|
+
const spacing = scale(useResolveTheme(appearance.spacing), size);
|
|
20
|
+
const foreground = useResolveTheme(appearance.colors.foreground);
|
|
21
|
+
const background = useResolveTheme(appearance.colors.background);
|
|
22
|
+
const tint = useResolveSolidColor(color ?? "primary:base");
|
|
23
|
+
const danger = useResolveSolidColor("danger:base");
|
|
24
|
+
const paints = useMemo(() => ({
|
|
25
|
+
palette: solidColors(tint, background, foreground),
|
|
26
|
+
neutral: solidColors(background, background, foreground),
|
|
27
|
+
danger: solidColors(danger, background, foreground)
|
|
28
|
+
}), [tint, background, foreground, danger]);
|
|
29
|
+
return {
|
|
30
|
+
spacing,
|
|
31
|
+
foreground,
|
|
32
|
+
background,
|
|
33
|
+
tint,
|
|
34
|
+
colored: color !== undefined,
|
|
35
|
+
danger,
|
|
36
|
+
paints,
|
|
37
|
+
radius: resolveRadius(radius, appearance),
|
|
38
|
+
fontSize: controlFontSizes[size],
|
|
39
|
+
height: Math.max(24, 24 + spacing),
|
|
40
|
+
gap: Math.max(4, spacing / 2)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function fieldStyle(theme, disabled = false, style) {
|
|
44
|
+
return {
|
|
45
|
+
...style,
|
|
46
|
+
...visualTransition,
|
|
47
|
+
display: "grid",
|
|
48
|
+
gap: theme.gap,
|
|
49
|
+
minWidth: 0,
|
|
50
|
+
color: theme.foreground,
|
|
51
|
+
fontFamily: "inherit",
|
|
52
|
+
fontSize: theme.fontSize,
|
|
53
|
+
lineHeight: 1.5,
|
|
54
|
+
opacity: disabled ? 0.46 : 1
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function controlPaint(theme, focused, invalid, hovered = false) {
|
|
58
|
+
const paints = invalid ? theme.paints.danger : theme.colored ? theme.paints.palette : theme.paints.neutral;
|
|
59
|
+
return focused ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
60
|
+
}
|
|
61
|
+
export function controlStyle(theme, focused, invalid, hovered = false, focusVisible = focused) {
|
|
62
|
+
const paint = controlPaint(theme, focused, invalid, hovered);
|
|
63
|
+
return {
|
|
64
|
+
...visualTransition,
|
|
65
|
+
appearance: "none",
|
|
66
|
+
boxSizing: "border-box",
|
|
67
|
+
width: "100%",
|
|
68
|
+
minWidth: 0,
|
|
69
|
+
height: theme.height,
|
|
70
|
+
paddingBlock: 0,
|
|
71
|
+
paddingInline: Math.max(8, theme.spacing),
|
|
72
|
+
border: 0,
|
|
73
|
+
borderRadius: theme.radius,
|
|
74
|
+
outline: focusVisible ? `2px solid ${theme.foreground}` : "none",
|
|
75
|
+
outlineOffset: 2,
|
|
76
|
+
...paint,
|
|
77
|
+
caretColor: paint.color,
|
|
78
|
+
font: "inherit",
|
|
79
|
+
lineHeight: 1.5
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function FieldLabel({ label }) {
|
|
83
|
+
return label == null ? null : _jsx(Label, { style: { fontWeight: 550 }, children: label });
|
|
84
|
+
}
|
|
85
|
+
export function FieldFeedback({ description, errorMessage, theme }) {
|
|
86
|
+
return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: 0.7 }, children: description }), _jsx(FieldError, { style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
|
|
87
|
+
}
|
|
@@ -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 = theme === "dark" ? appearance.foreground.dark : appearance.foreground.light;
|
|
89
|
+
const foreground = theme === "dark" ? appearance.colors.foreground.dark : appearance.colors.foreground.light;
|
|
90
90
|
const padding = 5;
|
|
91
91
|
const thumbSize = 6;
|
|
92
92
|
const size = thumbSize + padding * 2;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const textControlClass = "phreshos-ui-text-control";
|
|
3
|
+
/** Placeholder contrast follows the control's chosen text color, not browser gray. */
|
|
4
|
+
export default function FieldStyle() {
|
|
5
|
+
return _jsx("style", { href: "phreshos-react-ui-fields", precedence: "phreshos", children: `
|
|
6
|
+
.phreshos-ui-text-control::placeholder {
|
|
7
|
+
color: inherit;
|
|
8
|
+
opacity: 0.6;
|
|
9
|
+
}
|
|
10
|
+
` });
|
|
11
|
+
}
|
package/dist/flex.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { alignment, justification, resolveGap } from "./layout.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useAppearance } from "./appearance-provider.js";
|
|
5
5
|
/** A predictable Flexbox container with no visual appearance of its own. */
|
|
6
6
|
export const Flex = forwardRef(function Flex({ align, direction, gap, inline = false, justify, style, wrap, ...properties }, ref) {
|
|
7
|
-
const appearance =
|
|
7
|
+
const appearance = useAppearance();
|
|
8
8
|
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
9
|
...style,
|
|
10
10
|
display: inline ? "inline-flex" : "flex",
|
package/dist/grid.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { alignment, justification, resolveGap, tracks } from "./layout.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useAppearance } from "./appearance-provider.js";
|
|
5
5
|
/** A predictable CSS Grid container with no visual appearance of its own. */
|
|
6
6
|
export const Grid = forwardRef(function Grid({ align, columns, flow, gap, inline = false, justify, rows, style, ...properties }, ref) {
|
|
7
|
-
const appearance =
|
|
7
|
+
const appearance = useAppearance();
|
|
8
8
|
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
9
|
...style,
|
|
10
10
|
display: inline ? "inline-grid" : "grid",
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextControlProps } from "./text-control.js";
|
|
2
|
+
export type InputProps = TextControlProps;
|
|
3
|
+
/** A labeled single-line text control. onChange receives the string value. */
|
|
4
|
+
export declare const Input: import("react").ForwardRefExoticComponent<TextControlProps & import("react").RefAttributes<HTMLInputElement>>;
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Input as AriaInput, TextField } from "react-aria-components";
|
|
4
|
+
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
+
import { SurfaceField } from "./control-surface.js";
|
|
6
|
+
import FieldStyle, { textControlClass } from "./field-style.js";
|
|
7
|
+
/** A labeled single-line text control. onChange receives the string value. */
|
|
8
|
+
export const Input = forwardRef(function Input({ label, description, errorMessage, disabled, readOnly, required, invalid, size, color, radius, style, className, placeholder, 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(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
|
+
});
|
package/dist/main.d.ts
CHANGED
|
@@ -4,14 +4,24 @@
|
|
|
4
4
|
* Components enter this surface only after their implementation-independent
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
|
+
export { defaultAppearance } from "@phreshos/core";
|
|
7
8
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
|
|
8
9
|
export { Flex, type FlexProps } from "./flex.js";
|
|
9
10
|
export { Grid, type GridProps } from "./grid.js";
|
|
10
|
-
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";
|
|
12
|
+
export { defaultColor, type Color, type ColorLevel, type ColorScale } from "./color.js";
|
|
11
13
|
export { Panel, type PanelProps } from "./panel.js";
|
|
12
|
-
export { Button, type ButtonProps } from "./button.js";
|
|
14
|
+
export { Button, type ButtonColor, type ButtonProps } from "./button.js";
|
|
15
|
+
export { Input, type InputProps } from "./input.js";
|
|
16
|
+
export { Textarea, type TextareaProps } from "./textarea.js";
|
|
17
|
+
export { Checkbox, type CheckboxProps } from "./checkbox.js";
|
|
18
|
+
export { Radio, RadioGroup, type RadioProps, type RadioGroupProps } from "./radio.js";
|
|
19
|
+
export { Switch, type SwitchProps } from "./switch.js";
|
|
20
|
+
export { Select, type SelectProps, type SelectOption } from "./select.js";
|
|
21
|
+
export { Slider, type SliderProps } from "./slider.js";
|
|
22
|
+
export type { ControlColor } from "./control.js";
|
|
13
23
|
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
14
24
|
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
|
15
25
|
export { resolveSpacing, type Spacing } from "./spacing.js";
|
|
16
|
-
export { useColor
|
|
26
|
+
export { useColor } from "./color.js";
|
|
17
27
|
export { useScale, type NumericScale, type ScaleLevel } from "./scale.js";
|
package/dist/main.js
CHANGED
|
@@ -4,12 +4,21 @@
|
|
|
4
4
|
* Components enter this surface only after their implementation-independent
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
|
+
export { defaultAppearance } from "@phreshos/core";
|
|
7
8
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme } from "./appearance-provider.js";
|
|
8
9
|
export { Flex } from "./flex.js";
|
|
9
10
|
export { Grid } from "./grid.js";
|
|
10
11
|
export { Surface } from "./surface.js";
|
|
12
|
+
export { defaultColor } from "./color.js";
|
|
11
13
|
export { Panel } from "./panel.js";
|
|
12
14
|
export { Button } from "./button.js";
|
|
15
|
+
export { Input } from "./input.js";
|
|
16
|
+
export { Textarea } from "./textarea.js";
|
|
17
|
+
export { Checkbox } from "./checkbox.js";
|
|
18
|
+
export { Radio, RadioGroup } from "./radio.js";
|
|
19
|
+
export { Switch } from "./switch.js";
|
|
20
|
+
export { Select } from "./select.js";
|
|
21
|
+
export { Slider } from "./slider.js";
|
|
13
22
|
export { resolveRadius } from "./radius.js";
|
|
14
23
|
export { resolveSpacing } from "./spacing.js";
|
|
15
24
|
export { useColor } from "./color.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ScaleLevel } from "./scale.js";
|
|
2
|
+
/** Optional values of visual substance, expressed directly or relative to Appearance. */
|
|
3
|
+
export interface MaterialOptions {
|
|
4
|
+
readonly opacity?: ScaleLevel | number;
|
|
5
|
+
readonly backdrop?: ScaleLevel | number;
|
|
6
|
+
readonly grain?: ScaleLevel | number;
|
|
7
|
+
readonly grainAmount?: ScaleLevel | number;
|
|
8
|
+
readonly distortion?: ScaleLevel | number;
|
|
9
|
+
readonly saturation?: ScaleLevel | number;
|
|
10
|
+
}
|
|
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. */
|
|
16
|
+
export declare function useMaterialOptions(values?: MaterialOptions): {
|
|
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 useMaterialOptions>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appearanceLimits } from "@phreshos/core";
|
|
2
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
3
|
+
import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
|
|
4
|
+
/** Resolves material values from the current Appearance without knowing any host geometry. */
|
|
5
|
+
export function useMaterialOptions(values = {}) {
|
|
6
|
+
const appearance = useAppearance();
|
|
7
|
+
const defaults = useResolveTheme(appearance.material);
|
|
8
|
+
const limits = appearanceLimits.material;
|
|
9
|
+
return {
|
|
10
|
+
opacity: resolve(values.opacity, defaults.opacity, limits.opacity),
|
|
11
|
+
backdrop: resolve(values.backdrop, defaults.backdrop, limits.backdrop),
|
|
12
|
+
grain: resolve(values.grain, defaults.grain, limits.grain),
|
|
13
|
+
grainAmount: resolve(values.grainAmount, defaults.grainAmount, limits.grainAmount),
|
|
14
|
+
distortion: resolve(values.distortion, defaults.distortion, limits.distortion),
|
|
15
|
+
saturation: resolve(values.saturation, defaults.saturation, limits.saturation, scaleMultiplier)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolve(value, base, range, derive = scale) {
|
|
19
|
+
const resolved = isScaleLevel(value) ? derive(base, value) : value ?? base;
|
|
20
|
+
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
21
|
+
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface MaterialPaintProps {
|
|
2
|
+
readonly color: string;
|
|
3
|
+
readonly distortion: number;
|
|
4
|
+
readonly grain: number;
|
|
5
|
+
readonly grainAmount: number;
|
|
6
|
+
readonly identity: string;
|
|
7
|
+
readonly opacity: number;
|
|
8
|
+
}
|
|
9
|
+
/** The fill, refraction definition, and grain of one Surface. */
|
|
10
|
+
export declare function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }: MaterialPaintProps): import("react").JSX.Element | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { paintTransition } from "./motion-style.js";
|
|
4
|
+
/** The fill, refraction definition, and grain of one Surface. */
|
|
5
|
+
export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }) {
|
|
6
|
+
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
7
|
+
const hasPaint = opacity > 0;
|
|
8
|
+
const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
|
|
9
|
+
const hasDistortion = distortion > 0;
|
|
10
|
+
const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
11
|
+
if (!hasPaint && !hasDistortion)
|
|
12
|
+
return null;
|
|
13
|
+
return _jsxs("svg", { "data-material-paint": "", "aria-hidden": "true", focusable: "false", style: {
|
|
14
|
+
position: "absolute",
|
|
15
|
+
zIndex: -1,
|
|
16
|
+
inset: 0,
|
|
17
|
+
display: "block",
|
|
18
|
+
width: "100%",
|
|
19
|
+
height: "100%",
|
|
20
|
+
overflow: "hidden",
|
|
21
|
+
borderRadius: "inherit",
|
|
22
|
+
pointerEvents: "none"
|
|
23
|
+
}, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-material-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsxs("g", { "data-material-fill": "", opacity: opacity, style: paintTransition, children: [_jsx("rect", { "data-material-base": "", width: "100%", height: "100%", style: { ...paintTransition, fill: color } }), hasGrain && _jsx("rect", { "data-material-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] });
|
|
24
|
+
}
|
|
25
|
+
function DistortionFilter({ distortion, identity }) {
|
|
26
|
+
return _jsxs("filter", { id: `${identity}-distortion`, "data-material-distortion": "", x: "-20%", y: "-20%", width: "140%", height: "140%", colorInterpolationFilters: "sRGB", children: [_jsx("feTurbulence", { "data-material-distortion-noise": "", "data-material-distortion-field": "organic", type: "fractalNoise", baseFrequency: "0.008 0.008", numOctaves: 2, seed: 92, result: `${identity}-organic-noise` }), _jsx("feGaussianBlur", { in: `${identity}-organic-noise`, stdDeviation: 2, result: `${identity}-organic-noise-blurred` }), _jsx("feDisplacementMap", { "data-material-distortion-stage": "organic", in: "SourceGraphic", in2: `${identity}-organic-noise-blurred`, scale: distortion, xChannelSelector: "R", yChannelSelector: "G" })] });
|
|
27
|
+
}
|
|
28
|
+
function grainTone(color, tone, intensity) {
|
|
29
|
+
const channel = Math.round(tone / (toneCount - 1) * 255);
|
|
30
|
+
const percentage = Math.round(intensity * 10_000) / 100;
|
|
31
|
+
return `color-mix(in srgb, ${color} ${100 - percentage}%, rgb(${channel} ${channel} ${channel}) ${percentage}%)`;
|
|
32
|
+
}
|
|
33
|
+
function grainPaths(seed, amount) {
|
|
34
|
+
const tones = Array.from({ length: toneCount }, () => []);
|
|
35
|
+
for (let y = 0; y < patternSize; y += 1) {
|
|
36
|
+
for (let x = 0; x < patternSize; x += 1) {
|
|
37
|
+
const pointX = x + seed * 41;
|
|
38
|
+
const pointY = y + seed * 17;
|
|
39
|
+
const presence = shaderHash(pointX + 71.9, pointY + 13.7);
|
|
40
|
+
if (presence > amount)
|
|
41
|
+
continue;
|
|
42
|
+
const fine = shaderHash(Math.floor(pointX * 1.18), Math.floor(pointY * 1.18));
|
|
43
|
+
const clustered = shaderHash(Math.floor(pointX * 0.47) + 31.7, Math.floor(pointY * 0.47) + 31.7);
|
|
44
|
+
const value = clamp(fine * 0.8 + clustered * 0.2, 0, 1);
|
|
45
|
+
const tone = Math.min(toneCount - 1, Math.floor(value * toneCount));
|
|
46
|
+
tones[tone]?.push(`M${x} ${y}h1v1h-1z`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tones.map(tone => tone.join(""));
|
|
50
|
+
}
|
|
51
|
+
function shaderHash(x, y) {
|
|
52
|
+
let red = fract(x * 0.1031);
|
|
53
|
+
let green = fract(y * 0.1031);
|
|
54
|
+
let blue = fract(x * 0.1031);
|
|
55
|
+
const product = red * (green + 33.33) + green * (blue + 33.33) + blue * (red + 33.33);
|
|
56
|
+
red += product;
|
|
57
|
+
green += product;
|
|
58
|
+
blue += product;
|
|
59
|
+
return fract((red + green) * blue);
|
|
60
|
+
}
|
|
61
|
+
function seedFrom(value) {
|
|
62
|
+
let seed = 2166136261;
|
|
63
|
+
for (const character of value) {
|
|
64
|
+
seed ^= character.codePointAt(0) ?? 0;
|
|
65
|
+
seed = Math.imul(seed, 16777619);
|
|
66
|
+
}
|
|
67
|
+
return (seed >>> 0) % 997 + 1;
|
|
68
|
+
}
|
|
69
|
+
function fract(value) {
|
|
70
|
+
return value - Math.floor(value);
|
|
71
|
+
}
|
|
72
|
+
function clamp(value, minimum, maximum) {
|
|
73
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
74
|
+
}
|
|
75
|
+
const patternSize = 64;
|
|
76
|
+
const toneCount = 16;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The same fixed timing for Motion-driven control movements. */
|
|
2
|
+
export declare const controlTransition: {
|
|
3
|
+
readonly type: "tween";
|
|
4
|
+
readonly duration: 0.12;
|
|
5
|
+
readonly ease: "easeOut";
|
|
6
|
+
};
|
|
7
|
+
/** Host opacity and filters are excluded: either can change backdrop sampling. */
|
|
8
|
+
export declare const visualTransition: {
|
|
9
|
+
transitionProperty: "background-color, color, border-color, border-radius";
|
|
10
|
+
transitionDuration: "var(--phreshos-ui-motion-duration, 120ms)";
|
|
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
|
+
};
|
|
19
|
+
export declare const overlayMotionClass = "phreshos-ui-overlay";
|
|
20
|
+
/** React hoists and deduplicates this stylesheet, including nested providers and portals. */
|
|
21
|
+
export default function MotionStyle(): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
const timing = {
|
|
3
|
+
transitionDuration: "var(--phreshos-ui-motion-duration, 120ms)",
|
|
4
|
+
transitionTimingFunction: "ease-out"
|
|
5
|
+
};
|
|
6
|
+
/** The same fixed timing for Motion-driven control movements. */
|
|
7
|
+
export const controlTransition = { type: "tween", duration: 0.12, ease: "easeOut" };
|
|
8
|
+
/** Host opacity and filters are excluded: either can change backdrop sampling. */
|
|
9
|
+
export const visualTransition = {
|
|
10
|
+
...timing,
|
|
11
|
+
transitionProperty: "background-color, color, border-color, border-radius"
|
|
12
|
+
};
|
|
13
|
+
/** Radius is inherited from the host; only the painted values transition here. */
|
|
14
|
+
export const paintTransition = {
|
|
15
|
+
...timing,
|
|
16
|
+
transitionProperty: "fill, stroke, opacity"
|
|
17
|
+
};
|
|
18
|
+
export const overlayMotionClass = "phreshos-ui-overlay";
|
|
19
|
+
const stylesheet = `
|
|
20
|
+
:root { --phreshos-ui-motion-duration: 120ms; }
|
|
21
|
+
|
|
22
|
+
.phreshos-ui-overlay { --phreshos-ui-overlay-x: 0px; --phreshos-ui-overlay-y: -4px; }
|
|
23
|
+
.phreshos-ui-overlay[data-placement="top"] { --phreshos-ui-overlay-y: 4px; }
|
|
24
|
+
.phreshos-ui-overlay[data-placement="left"] { --phreshos-ui-overlay-x: 4px; --phreshos-ui-overlay-y: 0px; }
|
|
25
|
+
.phreshos-ui-overlay[data-placement="right"] { --phreshos-ui-overlay-x: -4px; --phreshos-ui-overlay-y: 0px; }
|
|
26
|
+
.phreshos-ui-overlay[data-entering] {
|
|
27
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration, 120ms) ease-out both;
|
|
28
|
+
}
|
|
29
|
+
.phreshos-ui-overlay[data-exiting] {
|
|
30
|
+
animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration, 120ms) ease-out reverse both;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
@keyframes phreshos-ui-overlay-enter {
|
|
34
|
+
from { opacity: 0; translate: var(--phreshos-ui-overlay-x) var(--phreshos-ui-overlay-y); }
|
|
35
|
+
to { opacity: 1; translate: 0 0; }
|
|
36
|
+
}
|
|
37
|
+
@media (prefers-reduced-motion: reduce) {
|
|
38
|
+
:root { --phreshos-ui-motion-duration: 0ms; }
|
|
39
|
+
.phreshos-ui-overlay[data-entering], .phreshos-ui-overlay[data-exiting] { animation: none; }
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
/** React hoists and deduplicates this stylesheet, including nested providers and portals. */
|
|
43
|
+
export default function MotionStyle() {
|
|
44
|
+
return _jsx("style", { href: "phreshos-react-ui-motion", precedence: "phreshos", children: stylesheet });
|
|
45
|
+
}
|
package/dist/panel.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export interface PanelProps extends SurfaceProps {
|
|
|
6
6
|
readonly contentProps?: Omit<SurfaceProps, "children">;
|
|
7
7
|
}
|
|
8
8
|
/** Owns the shared shell layout, not positioning, interaction, or lifecycle. */
|
|
9
|
-
export declare const Panel: import("react").ForwardRefExoticComponent<PanelProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
9
|
+
export declare const Panel: import("react").ForwardRefExoticComponent<Omit<PanelProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/radio.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { RadioGroupProps as AriaRadioGroupProps, RadioFieldProps } from "react-aria-components";
|
|
3
|
+
import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
|
|
4
|
+
import type { MaterialOverrides } from "./material-options.js";
|
|
5
|
+
export interface RadioGroupProps extends Omit<AriaRadioGroupProps, ControlOverrides | "isReadOnly">, ControlProps, FieldProps, MaterialOverrides {
|
|
6
|
+
readonly children: ReactNode;
|
|
7
|
+
readonly readOnly?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RadioProps extends Omit<RadioFieldProps, ControlOverrides>, ControlProps, Pick<FieldProps, "label" | "description">, MaterialOverrides {
|
|
10
|
+
}
|
|
11
|
+
/** One string value selected from its Radio children, with native arrow-key navigation. */
|
|
12
|
+
export declare const RadioGroup: import("react").ForwardRefExoticComponent<RadioGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
13
|
+
/** An option in a RadioGroup. Selection and validation belong to the group. */
|
|
14
|
+
export declare const Radio: import("react").ForwardRefExoticComponent<RadioProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/radio.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, forwardRef, useContext } from "react";
|
|
3
|
+
import { RadioGroup as AriaRadioGroup, RadioField, RadioButton, Text } from "react-aria-components";
|
|
4
|
+
import { FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
+
import { ToggleIndicator, toggleStyle } from "./toggle-indicator.js";
|
|
6
|
+
const RadioStyle = createContext({});
|
|
7
|
+
/** One string value selected from its Radio children, with native arrow-key navigation. */
|
|
8
|
+
export const RadioGroup = forwardRef(function RadioGroup({ label, description, errorMessage, children, disabled, readOnly, required, invalid, size, color, style, material, orientation = "vertical", ...properties }, ref) {
|
|
9
|
+
const theme = useControlTheme({ size, color });
|
|
10
|
+
return _jsxs(AriaRadioGroup, { ...properties, ref: ref, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, orientation: orientation, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldLabel, { label: label }), _jsx(RadioStyle.Provider, { value: { size, color, disabled, material }, children: _jsx("div", { style: { display: "flex", flexDirection: orientation === "vertical" ? "column" : "row", gap: theme.gap, flexWrap: "wrap" }, children: children }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
|
|
11
|
+
});
|
|
12
|
+
/** An option in a RadioGroup. Selection and validation belong to the group. */
|
|
13
|
+
export const Radio = forwardRef(function Radio({ label, description, disabled, size, color, style, material, ...properties }, ref) {
|
|
14
|
+
const inherited = useContext(RadioStyle);
|
|
15
|
+
const theme = useControlTheme({ size: size ?? inherited.size, color: color ?? inherited.color });
|
|
16
|
+
return _jsxs(RadioField, { ...properties, ref: ref, isDisabled: disabled, style: state => fieldStyle(theme, state.isDisabled && !inherited.disabled, style), children: [_jsx(RadioButton, { style: state => toggleStyle(theme, state.isDisabled, state.isReadOnly), children: state => _jsxs(_Fragment, { children: [_jsx(ToggleIndicator, { kind: "radio", material: material ?? inherited.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] }) }), description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
|
|
17
|
+
});
|
package/dist/select.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SelectProps as AriaSelectProps } 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-options.js";
|
|
5
|
+
export interface SelectOption {
|
|
6
|
+
readonly value: string;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SelectProps extends Omit<AriaSelectProps<SelectOption>, ControlOverrides | "value" | "defaultValue" | "onChange" | "selectedKey" | "defaultSelectedKey" | "onSelectionChange" | "disabledKeys" | "selectionMode">, ControlProps, FieldProps, RadiusProps, MaterialOverrides {
|
|
11
|
+
readonly options: readonly SelectOption[];
|
|
12
|
+
readonly value?: string | null;
|
|
13
|
+
readonly defaultValue?: string | null;
|
|
14
|
+
readonly onChange?: (value: string | null) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
|
|
17
|
+
export declare const Select: import("react").ForwardRefExoticComponent<SelectProps & import("react").RefAttributes<HTMLDivElement>>;
|
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 { SurfaceButton } from "./control-surface.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(SurfaceButton, { 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
|
+
});
|