@phreshos/react-ui 0.1.17 → 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 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
- host the shared Material while retaining their native behavior. Color and material
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. Material and neutral
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
- `Material` is the raw visual substance: paint, opacity, frost, refraction, and
76
- grain. It fills the geometry of its nearest positioned container,
77
- inherits that container's radius, and adds no content, layout, interaction, or
78
- shadow. The container must establish its own geometry and isolation.
79
-
80
- ```tsx
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}>Direct values</Surface>
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
- `MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
98
- `distortion`, and `saturation`. It contains no color. `MaterialProps` keeps
99
- `color` beside `material?: MaterialOptions`, so paint selection never becomes a
100
- physical-material setting. Omitted material values follow `appearance.material`.
101
- Effect options accept a scale level or a direct number; opacity affects material
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
- Material-bearing controls expose the same separate `color` and `material` props.
94
+ `MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
95
+ `distortion`, and `saturation`. Surface accepts these properties directly because
96
+ it is the material-owning element; it does not accept a nested `material` prop.
97
+ Color remains a separate Surface 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.
@@ -146,7 +140,7 @@ Server SDK.
146
140
 
147
141
  Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
148
142
  Shared CSS transitions use a fixed 120ms ease-out timing for colors and corner
149
- radius. Material fill and opacity transition on the painted layers, not on the
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
146
  these changes immediate. No Appearance configuration is added for motion yet.
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,12 @@ 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 { MaterialButton } from "./control-material.js";
5
+ import { SurfaceButton } from "./control-surface.js";
6
6
  import { visualTransition } from "./motion-style.js";
7
7
  /** An Appearance-aware action with normalized pointer and keyboard behavior. */
8
8
  export const Button = forwardRef(function Button({ children, color, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, material, type = "button", ...properties }, ref) {
9
9
  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(MaterialButton, { native: native, material: material, paint: buttonPaint(theme, !disabled && !pending, state.isHovered, state.isPressed) }), style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
10
+ 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
11
  theme,
12
12
  disabled,
13
13
  isFocusVisible,
@@ -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;
@@ -4,14 +4,14 @@ type Paint = Readonly<{
4
4
  background: string;
5
5
  color: string;
6
6
  }>;
7
- /** A native button hosts Material without changing its native contract. */
8
- export declare function MaterialButton({ native, paint, material: options }: {
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 MaterialField({ paint, radius, children, material: options }: Readonly<{
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, ...options, as: "button", color: paint.background, 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, { ...options, as: "span", color: paint.background, radius: radius, style: { color: paint.color, display: "grid", minWidth: 0 }, children: children });
11
+ }
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 { MaterialField } from "./control-material.js";
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(MaterialField, { 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 })] });
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
@@ -8,8 +8,7 @@ export { defaultAppearance } from "@phreshos/core";
8
8
  export { AppearanceProvider, useAppearance, useResolveTheme, 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 { Material, type MaterialOptions, type MaterialOverrides, type MaterialProps } from "./material.js";
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
@@ -8,7 +8,6 @@ export { defaultAppearance } from "@phreshos/core";
8
8
  export { AppearanceProvider, useAppearance, useResolveTheme, 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
- /** Resolves Material values from the current Appearance without knowing any host geometry. */
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;
@@ -1,7 +1,7 @@
1
1
  import { appearanceLimits } from "@phreshos/core";
2
2
  import { useAppearance, useResolveTheme } from "./appearance-provider.js";
3
3
  import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
4
- /** Resolves Material values from the current Appearance without knowing any host geometry. */
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
7
  const defaults = useResolveTheme(appearance.material);
@@ -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 Material. */
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 {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { paintTransition } from "./motion-style.js";
4
- /** The fill, refraction definition, and grain of one Material. */
4
+ /** The fill, refraction definition, and grain of one Surface. */
5
5
  export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }) {
6
6
  const seed = useMemo(() => seedFrom(identity), [identity]);
7
7
  const hasPaint = opacity > 0;
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/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,14 +3,14 @@ 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 { MaterialButton } from "./control-material.js";
6
+ import { SurfaceButton } from "./control-surface.js";
7
7
  import { Surface } from "./surface.js";
8
8
  import { controlTransition, overlayMotionClass, visualTransition } 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
- 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 => ({
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
14
  ...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
15
15
  display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
16
16
  cursor: disabled ? "not-allowed" : "pointer", textAlign: "start"
@@ -1,5 +1,8 @@
1
- import type { ResolvedMaterial } from "./material.js";
2
- type EdgeMaterial = Pick<ResolvedMaterial, "color" | "foreground" | "opacity">;
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.d.ts CHANGED
@@ -1,7 +1,23 @@
1
- import type { ComponentPropsWithRef } from "react";
2
- import { type MaterialProps } from "./material.js";
1
+ import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactElement, ReactNode } from "react";
2
+ import { type Color } from "./color.js";
3
+ import { type MaterialOptions } from "./material-options.js";
3
4
  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 {
5
+ export type { MaterialOptions } from "./material-options.js";
6
+ export interface SurfaceOwnProps extends MaterialOptions, RadiusProps {
7
+ readonly color?: Color;
6
8
  }
7
- export declare const Surface: import("react").ForwardRefExoticComponent<Omit<SurfaceProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
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,62 @@
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 { MaterialLayer, useResolvedMaterial } from "./material.js";
5
- import { visualTransition } from "./motion-style.js";
2
+ import { createElement, forwardRef, useId } from "react";
3
+ import { useAppearance, useResolveTheme } 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, { visualTransition } from "./motion-style.js";
6
8
  import { resolveRadius } from "./radius.js";
7
9
  import { SurfaceEdge } from "./surface-edge.js";
8
- export const Surface = forwardRef(function Surface({ color, material: options, radius, children, style, ...properties }, ref) {
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, opacity, backdrop, grain, grainAmount, distortion, saturation, radius, children, style, ...properties }, ref) {
9
12
  const appearance = useAppearance();
10
- const material = useResolvedMaterial({ color, material: options });
13
+ const material = useResolvedSurface(color, { opacity, backdrop, grain, grainAmount, distortion, saturation });
11
14
  const resolvedRadius = resolveRadius(radius ?? "medium", appearance);
12
- return _jsxs("div", { ...properties, ref: ref, style: {
15
+ return createElement(Element, {
16
+ ...properties,
17
+ ref,
18
+ style: {
13
19
  ...visualTransition,
14
- background: "transparent",
15
- color: material.foreground,
16
20
  ...style,
21
+ background: "transparent",
22
+ color: style?.color ?? material.foreground,
17
23
  borderRadius: radius === undefined ? style?.borderRadius ?? resolvedRadius : resolvedRadius,
18
24
  position: style?.position ?? "relative",
19
25
  isolation: "isolate"
20
- }, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
26
+ }
27
+ }, _jsx(SurfaceLayers, { material: material }), _jsx(SurfaceEdge, { material: material }), children);
21
28
  });
29
+ export const Surface = SurfaceRoot;
30
+ function useResolvedSurface(color, options) {
31
+ const appearance = useAppearance();
32
+ const material = useMaterialOptions(options);
33
+ return {
34
+ ...material,
35
+ color: useResolveColor(color),
36
+ foreground: useResolveTheme(appearance.colors.foreground)
37
+ };
38
+ }
39
+ const layerStyle = {
40
+ position: "absolute",
41
+ inset: 0,
42
+ borderRadius: "inherit",
43
+ pointerEvents: "none"
44
+ };
45
+ function SurfaceLayers({ material }) {
46
+ const { backdrop, saturation, distortion } = material;
47
+ const frost = [
48
+ backdrop === 0 ? "" : `blur(${backdrop}px)`,
49
+ saturation === 1 ? "" : `saturate(${saturation})`
50
+ ].filter(Boolean).join(" ");
51
+ const identity = `phresh-material-${useId().replaceAll(":", "")}`;
52
+ 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 })] });
53
+ }
54
+ /** Keep refraction and native frost in independent compositor passes. */
55
+ function BackdropLayer({ filter, name, zIndex }) {
56
+ return _jsx("span", { "data-material-backdrop": name, "aria-hidden": "true", style: {
57
+ ...layerStyle,
58
+ zIndex,
59
+ backdropFilter: filter,
60
+ WebkitBackdropFilter: filter
61
+ } });
62
+ }
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;
@@ -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 { MaterialField } from "./control-material.js";
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(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 => ({
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,
@@ -1,9 +1,8 @@
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 { MaterialLayer, useResolvedMaterial } from "./material.js";
5
- import { SurfaceEdge } from "./surface-edge.js";
6
4
  import { controlTransition, visualTransition } 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();
@@ -16,11 +15,9 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
16
15
  const switching = kind === "switch";
17
16
  const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
18
17
  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: {
18
+ return _jsx(Surface, { as: motion.span, ...options, color: paint.background, "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
19
  color: paint.color,
22
20
  ...visualTransition,
23
- background: "transparent",
24
21
  display: "inline-grid",
25
22
  placeItems: "center",
26
23
  position: "relative",
@@ -32,12 +29,12 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
32
29
  border: "none",
33
30
  outline: focused ? `2px solid ${theme.foreground}` : "none",
34
31
  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 } })] }) })] });
32
+ }, children: 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: {
33
+ ...visualTransition,
34
+ height: diameter - 6, borderRadius: diameter,
35
+ background: paint.color
36
+ } }) : _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" } })
37
+ : _jsxs("svg", { width: diameter - 4, height: diameter - 4, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx(motion.path, { d: "m3 8 3 3 7-7", initial: false, animate: { pathLength: selected && !indeterminate ? 1 : 0, opacity: selected && !indeterminate ? 1 : 0 }, transition: { ...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
38
  }
42
39
  export function toggleStyle(theme, disabled, readOnly) {
43
40
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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
- }
@@ -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
- }