@phreshos/react-ui 0.1.28 → 0.1.30

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/dist/popover.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Heading as AriaHeading, Popover as AriaPopover } from "react-aria-components";
4
- import { useAppearance } from "./ui-provider.js";
4
+ import { useResolvedAppearance } from "./appearance-context.js";
5
5
  import { Button } from "./button.js";
6
- import { overlayMotionClass, useOverlayTransition } from "./motion-style.js";
6
+ import MotionStyle, { overlayMotionClass, overlayTransition } from "./motion-style.js";
7
7
  import { scale } from "./scale.js";
8
8
  import { Surface } from "./surface.js";
9
9
  import { resolveDirection, useDirection } from "./direction.js";
@@ -15,16 +15,17 @@ export const PopoverTrigger = forwardRef(function PopoverTrigger(properties, ref
15
15
  return _jsx(Button, { ...properties, ref: ref });
16
16
  });
17
17
  export const PopoverContent = forwardRef(function PopoverContent({ children, className, color, material, radius, shadow, style, offset, placement = "bottom", ...properties }, ref) {
18
- const inset = scale(useAppearance().spacing, "small");
19
- const transition = useOverlayTransition();
18
+ const resolved = useResolvedAppearance();
19
+ const inset = scale(resolved.appearance.spacing, "small");
20
+ const transition = overlayTransition(resolved.transaction, resolved.preferences.animations);
20
21
  const direction = resolveDirection(properties.dir, useDirection());
21
- return _jsx(AriaPopover, { ...properties, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
22
- boxSizing: "border-box",
23
- maxWidth: `calc(100vw - ${inset * 2}px)`,
24
- maxHeight: `calc(100vh - ${inset * 2}px)`,
25
- outline: "none",
26
- ...style
27
- }, children: children }) });
22
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaPopover, { ...properties, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
23
+ boxSizing: "border-box",
24
+ maxWidth: `calc(100vw - ${inset * 2}px)`,
25
+ maxHeight: `calc(100vh - ${inset * 2}px)`,
26
+ outline: "none",
27
+ ...style
28
+ }, children: children }) })] });
28
29
  });
29
30
  export const PopoverDialog = forwardRef(function PopoverDialog({ style, ...properties }, ref) {
30
31
  return _jsx(AriaDialog, { ...properties, ref: ref, style: { minWidth: 0, outline: "none", ...style } });
package/dist/radio.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Base UI accepts direction directly; React Aria derives these keys from locale.
2
3
  import { Radio as BaseRadio } from "@base-ui/react/radio";
3
4
  import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
4
5
  import { DirectionProvider as BaseDirectionProvider } from "@base-ui/react/direction-provider";
5
6
  import { createContext, forwardRef, useContext, useId, useState } from "react";
6
- import { fieldStyle, useControlTheme } from "./control.js";
7
+ import { controlFontWeight, controlOpacity, fieldStyle, useControlTheme } from "./control.js";
7
8
  import { ToggleIndicator, toggleStyle } from "./toggle-indicator.js";
8
9
  import { resolveDirection, useDirection } from "./direction.js";
9
10
  const RadioStyleContext = createContext(null);
@@ -18,7 +19,7 @@ export const RadioGroup = forwardRef(function RadioGroup({ label, description, e
18
19
  const root = _jsxs(BaseRadioGroup, { ...properties, ref: ref, className: className, value: value, defaultValue: defaultValue, onValueChange: next => {
19
20
  if (next != null)
20
21
  onChange?.(next);
21
- }, disabled: disabled, readOnly: readOnly, required: required, "aria-invalid": invalid || undefined, "aria-labelledby": ariaLabelledBy ?? (label != null ? labelId : undefined), "aria-describedby": describedBy, style: fieldStyle(theme, disabled, style), children: [label != null && _jsx("div", { id: labelId, style: { fontWeight: 550 }, children: label }), _jsx(RadioStyleContext.Provider, { value: { size, color, disabled, invalid, readOnly, material, shadow }, children: _jsx("div", { style: { display: "flex", flexDirection: orientation === "vertical" ? "column" : "row", gap: theme.gap, flexWrap: "wrap" }, children: children }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description }), invalid && errorMessage != null && _jsx("span", { id: errorId, style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
22
+ }, disabled: disabled, readOnly: readOnly, required: required, "aria-invalid": invalid || undefined, "aria-labelledby": ariaLabelledBy ?? (label != null ? labelId : undefined), "aria-describedby": describedBy, style: fieldStyle(theme, disabled, style), children: [label != null && _jsx("div", { id: labelId, style: { fontWeight: controlFontWeight }, children: label }), _jsx(RadioStyleContext.Provider, { value: { size, color, disabled, invalid, readOnly, material, shadow }, children: _jsx("div", { style: { display: "flex", flexDirection: orientation === "vertical" ? "column" : "row", gap: theme.gap, flexWrap: "wrap" }, children: children }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: controlOpacity.secondary }, children: description }), invalid && errorMessage != null && _jsx("span", { id: errorId, style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
22
23
  return _jsx(BaseDirectionProvider, { direction: direction, children: root });
23
24
  });
24
25
  /** An option in a RadioGroup. Selection and validation belong to the group. */
@@ -38,5 +39,5 @@ export const Radio = forwardRef(function Radio({ label, description, value, disa
38
39
  }, onBlur: event => {
39
40
  setFocusVisible(false);
40
41
  onBlur?.(event);
41
- }, style: state => toggleStyle(theme, state.disabled, state.readOnly), render: (native, state) => _jsxs("span", { ...native, children: [_jsx(ToggleIndicator, { kind: "radio", material: material ?? inherited.material, shadow: shadow ?? inherited.shadow, theme: theme, selected: state.checked, focused: focusVisible, invalid: inherited.invalid, hovered: !state.disabled && !state.readOnly && hovered, pressed: !state.disabled && !state.readOnly && pressed }), label] }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
42
+ }, style: state => toggleStyle(theme, state.disabled, state.readOnly), render: (native, state) => _jsxs("span", { ...native, children: [_jsx(ToggleIndicator, { kind: "radio", material: material ?? inherited.material, shadow: shadow ?? inherited.shadow, theme: theme, selected: state.checked, focused: focusVisible, invalid: inherited.invalid, hovered: !state.disabled && !state.readOnly && hovered, pressed: !state.disabled && !state.readOnly && pressed }), label] }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: controlOpacity.secondary }, children: description })] });
42
43
  });
@@ -2,43 +2,50 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
3
3
  import { DirectionProvider as BaseDirectionProvider } from "@base-ui/react/direction-provider";
4
4
  import { forwardRef, useState } from "react";
5
- import { useAppearance, useThemedValue } from "./ui-provider.js";
5
+ import { useResolvedAppearance } from "./appearance-context.js";
6
6
  import { colorOpacity } from "./color.js";
7
- import { useTransitionTiming } from "./motion-style.js";
7
+ import { transitionTiming } from "./motion-style.js";
8
8
  import { resolveDirection, useDirection } from "./direction.js";
9
9
  /** A native scrolling viewport with locally owned React UI scrollbars. */
10
10
  export const ScrollArea = forwardRef(function ScrollArea({ axis = "vertical", children, onScroll, style, viewportRef, ...properties }, ref) {
11
- const appearance = useAppearance();
11
+ const resolved = useResolvedAppearance();
12
+ const { appearance } = resolved;
12
13
  const direction = resolveDirection(properties.dir, useDirection());
13
- const foreground = useThemedValue(appearance.colors).foreground;
14
+ const foreground = resolved.colors.foreground;
14
15
  const radius = Math.min(appearance.radius, 8);
16
+ const thickness = Math.max(8, appearance.spacing);
17
+ const inset = Math.max(2, thickness / 3);
15
18
  const vertical = axis === "vertical" || axis === "both";
16
19
  const horizontal = axis === "horizontal" || axis === "both";
20
+ const transition = transitionTiming(resolved.transaction, resolved.preferences.animations);
17
21
  const root = _jsxs(BaseScrollArea.Root, { ...properties, "data-phreshos-scroll-area": "", ref: ref, style: {
18
22
  ...style,
23
+ display: "flex",
24
+ flexDirection: "column",
19
25
  position: style?.position ?? "relative",
20
26
  minWidth: style?.minWidth ?? 0,
21
27
  minHeight: style?.minHeight ?? 0,
22
28
  overflow: "hidden"
23
29
  }, children: [_jsx(BaseScrollArea.Viewport, { "data-phreshos-scroll-area-viewport": "", ref: viewportRef, onScroll: onScroll, style: {
30
+ flex: "1 1 auto",
31
+ minHeight: 0,
24
32
  width: "100%",
25
33
  height: "100%",
26
34
  overflowX: horizontal ? "scroll" : "hidden",
27
35
  overflowY: vertical ? "scroll" : "hidden"
28
- }, children: _jsx(BaseScrollArea.Content, { "data-phreshos-scroll-area-content": "", style: vertical && !horizontal ? { minWidth: "100%", width: "100%" } : undefined, children: children }) }), vertical && _jsx(Scrollbar, { orientation: "vertical", foreground: foreground, radius: radius }), horizontal && _jsx(Scrollbar, { orientation: "horizontal", foreground: foreground, radius: radius }), vertical && horizontal && _jsx(BaseScrollArea.Corner, { style: { background: "transparent" } })] });
36
+ }, children: _jsx(BaseScrollArea.Content, { "data-phreshos-scroll-area-content": "", style: vertical && !horizontal ? { minWidth: "100%", width: "100%" } : undefined, children: children }) }), vertical && _jsx(Scrollbar, { orientation: "vertical", foreground: foreground, radius: radius, thickness: thickness, inset: inset, transition: transition }), horizontal && _jsx(Scrollbar, { orientation: "horizontal", foreground: foreground, radius: radius, thickness: thickness, inset: inset, transition: transition }), vertical && horizontal && _jsx(BaseScrollArea.Corner, { style: { background: "transparent" } })] });
29
37
  return _jsx(BaseDirectionProvider, { direction: direction, children: root });
30
38
  });
31
- function Scrollbar({ orientation, foreground, radius }) {
39
+ function Scrollbar({ orientation, foreground, radius, thickness, inset, transition }) {
32
40
  const [hovered, setHovered] = useState(false);
33
- const transition = useTransitionTiming();
34
41
  const vertical = orientation === "vertical";
35
42
  return _jsx(BaseScrollArea.Scrollbar, { "data-phreshos-scroll-area-scrollbar": "", orientation: orientation, keepMounted: true, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), style: state => {
36
43
  const present = vertical ? state.hasOverflowY : state.hasOverflowX;
37
44
  return {
38
45
  boxSizing: "border-box",
39
- width: vertical ? 16 : undefined,
40
- height: vertical ? undefined : 16,
41
- padding: 5,
46
+ width: vertical ? thickness : undefined,
47
+ height: vertical ? undefined : thickness,
48
+ padding: inset,
42
49
  opacity: present && (state.hovering || state.scrolling) ? 1 : 0,
43
50
  pointerEvents: present && (state.hovering || state.scrolling) ? "auto" : "none",
44
51
  transitionDuration: transition.transitionDuration,
package/dist/select.js CHANGED
@@ -1,48 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
3
- import { motion } 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";
2
+ import { forwardRef, useMemo } from "react";
3
+ import { Select as AriaSelect, Button, SelectValue, ListBox, ListBoxItem } from "react-aria-components";
4
+ import { controlOpacity, controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
6
5
  import { SurfaceButton } from "./control-surface.js";
7
- import { Surface } from "./surface.js";
8
6
  import { ScrollArea } from "./scroll-area.js";
9
- import { overlayMotionClass, useControlTransition, useOverlayTransition } from "./motion-style.js";
7
+ import { PopoverContent } from "./popover.js";
10
8
  import { resolveDirection, useDirection } from "./direction.js";
11
- import { resolveDirectionalPlacement } from "./overlay-placement.js";
12
9
  /** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
13
10
  export const Select = forwardRef(function Select({ label, description, errorMessage, disabled, required, invalid, options, value, defaultValue, onChange, size, color, radius, style, material, shadow, ...properties }, ref) {
14
11
  const theme = useControlTheme({ size, color, radius });
15
- const transition = useControlTransition();
16
- const overlayTransition = useOverlayTransition();
17
12
  const direction = resolveDirection(properties.dir, useDirection());
18
- const optionDependencies = [
19
- theme.transition.transitionDuration,
20
- theme.transition.transitionTimingFunction,
21
- theme.transition.transitionProperty,
22
- theme.gap,
23
- theme.height,
24
- theme.spacing,
25
- theme.radius,
26
- theme.focusColor,
27
- theme.foreground,
28
- theme.paints.palette.rest.background,
29
- theme.paints.palette.rest.color,
30
- theme.paints.palette.hover.background,
31
- theme.paints.palette.hover.color
32
- ];
33
- 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, shadow: shadow, paint: controlPaint(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered) }), style: button => ({
13
+ const disabledKeys = useMemo(() => options.filter(option => option.disabled).map(option => option.value), [options]);
14
+ return _jsx(AriaSelect, { ...properties, ref: ref, value: value, defaultValue: defaultValue, onChange: key => onChange?.(key == null ? null : String(key)), disabledKeys: disabledKeys, 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, shadow: shadow, paint: controlPaint(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered) }), style: button => ({
34
15
  ...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
35
16
  display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
36
17
  cursor: disabled ? "not-allowed" : "pointer", textAlign: "start"
37
- }), children: [_jsx(SelectValue, { style: ({ isPlaceholder }) => ({ opacity: isPlaceholder ? 0.6 : 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }) }), _jsx(motion.svg, { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", initial: false, animate: { rotate: state.isOpen ? 180 : 0 }, transition: transition, style: { flexShrink: 0 }, children: _jsx("path", { d: "m2 4 4 4 4-4" }) })] }), _jsx(Popover, { dir: direction, className: overlayMotionClass, placement: resolveDirectionalPlacement("bottom start", direction), offset: theme.gap, maxHeight: 280 + theme.gap * 2, style: { ...overlayTransition, width: "var(--trigger-width)", maxWidth: "calc(100vw - 16px)" }, children: _jsx(Surface, { dir: direction, style: { padding: theme.gap, display: "flex", flexDirection: "column", maxHeight: "inherit", boxSizing: "border-box" }, children: _jsx(ScrollArea, { style: { flex: "1 1 auto", minHeight: 0 }, children: _jsx(ListBox, { items: options, dependencies: optionDependencies, shouldFocusOnHover: false, style: { display: "grid", gap: theme.gap, outline: "none", fontSize: theme.fontSize, color: theme.foreground }, children: option => _jsx(ListBoxItem, { id: option.value, textValue: option.label, style: item => ({
38
- ...theme.transition,
39
- display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
40
- minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
41
- borderRadius: theme.radius,
42
- outline: item.isFocused && !item.isSelected ? `1px solid ${theme.focusColor}` : "none", outlineOffset: 1,
43
- cursor: item.isDisabled ? "not-allowed" : "pointer",
44
- opacity: item.isDisabled ? 0.46 : 1,
45
- ...(item.isSelected ? item.isFocused ? theme.paints.palette.hover : theme.paints.palette.rest
46
- : { background: "transparent", color: theme.foreground })
47
- }), children: item => _jsxs(_Fragment, { children: [option.label, _jsx("span", { "aria-hidden": "true", children: item.isSelected ? "✓" : null })] }) }) }) }) }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] }) });
18
+ }), children: [_jsx(SelectValue, { style: ({ isPlaceholder }) => ({ opacity: isPlaceholder ? controlOpacity.placeholder : 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }) }), _jsx("svg", { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", style: { ...theme.transition, transitionProperty: "transform", flexShrink: 0, transform: `rotate(${state.isOpen ? 180 : 0}deg)` }, children: _jsx("path", { d: "m2 4 4 4 4-4" }) })] }), _jsx(PopoverContent, { dir: direction, placement: "bottom start", offset: theme.gap, style: {
19
+ width: "var(--trigger-width)",
20
+ maxHeight: `min(calc(100vh - ${theme.spacing * 2}px), ${theme.height * 8}px)`,
21
+ padding: theme.gap,
22
+ display: "flex",
23
+ flexDirection: "column"
24
+ }, children: _jsx(ScrollArea, { style: { flex: "1 1 auto", minHeight: 0 }, children: _jsx(ListBox, { shouldFocusOnHover: false, style: { display: "grid", gap: theme.gap, outline: "none", fontSize: theme.fontSize, color: theme.foreground }, children: options.map(option => _jsx(ListBoxItem, { id: option.value, textValue: option.label, style: item => ({
25
+ ...theme.transition,
26
+ display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
27
+ minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
28
+ borderRadius: theme.radius,
29
+ outline: item.isFocusVisible && !item.isSelected ? `1px solid ${theme.focusColor}` : "none", outlineOffset: 1,
30
+ cursor: item.isDisabled ? "not-allowed" : "pointer",
31
+ opacity: item.isDisabled ? controlOpacity.disabled : 1,
32
+ ...(item.isSelected
33
+ ? item.isPressed ? theme.paints.palette.pressed : item.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest
34
+ : item.isHovered ? theme.paints.neutral.hover : { background: "transparent", color: theme.foreground })
35
+ }), children: item => _jsxs(_Fragment, { children: [option.label, _jsx("span", { "aria-hidden": "true", children: item.isSelected ? "✓" : null })] }) }, option.value)) }) }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] }) });
48
36
  });
@@ -12,14 +12,20 @@ export interface ShadowOverrides {
12
12
  /** Omission or true uses Appearance defaults; false removes the shadow. */
13
13
  readonly shadow?: boolean | ShadowOptions;
14
14
  }
15
- /** Resolves shadow values from the active Appearance branch. */
16
- export declare function useShadowOptions(values?: ShadowOptions): {
15
+ /** Resolves overrides against an already selected shadow branch. */
16
+ export declare function resolveShadowOptions(values: ShadowOptions, defaults: Readonly<{
17
+ x: number;
18
+ y: number;
19
+ blur: number;
20
+ spread: number;
21
+ opacity: number;
22
+ }>): {
17
23
  x: number;
18
24
  y: number;
19
25
  blur: number;
20
26
  spread: number;
21
27
  opacity: number;
22
28
  };
23
- export type ResolvedShadow = ReturnType<typeof useShadowOptions>;
29
+ export type ResolvedShadow = ReturnType<typeof resolveShadowOptions>;
24
30
  /** Serializes one resolved neutral-black outer shadow. */
25
31
  export declare function shadowStyle({ x, y, blur, spread, opacity }: ResolvedShadow): string;
@@ -1,10 +1,7 @@
1
1
  import { appearanceLimits } from "./appearance.js";
2
- import { useAppearance, useThemedValue } from "./ui-provider.js";
3
2
  import { isScaleLevel, scale } from "./scale.js";
4
- /** Resolves shadow values from the active Appearance branch. */
5
- export function useShadowOptions(values = {}) {
6
- const appearance = useAppearance();
7
- const defaults = useThemedValue(appearance.shadow);
3
+ /** Resolves overrides against an already selected shadow branch. */
4
+ export function resolveShadowOptions(values, defaults) {
8
5
  const limits = appearanceLimits.shadow;
9
6
  return {
10
7
  x: resolve(values.x, defaults.x, limits.x),
package/dist/slider.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Base UI accepts direction directly; React Aria derives these keys from locale.
2
3
  import { Slider as BaseSlider } from "@base-ui/react/slider";
3
4
  import { DirectionProvider as BaseDirectionProvider } from "@base-ui/react/direction-provider";
4
5
  import { forwardRef, useEffect, useId, useRef, useState } from "react";
5
- import { fieldStyle, useControlTheme } from "./control.js";
6
+ import { controlFontWeight, controlOpacity, fieldStyle, useControlTheme } from "./control.js";
6
7
  import { resolveDirection, useDirection } from "./direction.js";
7
8
  /** One numeric value with pointer, touch, keyboard, and form support. */
8
9
  export const Slider = forwardRef(function Slider({ label, description, name, form, disabled, size, color, style, orientation = "horizontal", value, defaultValue, minValue = 0, maxValue = 100, step = 1, formatOptions, onChange, onChangeEnd, ...properties }, ref) {
@@ -14,7 +15,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, for
14
15
  const [uncontrolledValue, setUncontrolledValue] = useState(initialValue.current);
15
16
  const [hovered, setHovered] = useState(false);
16
17
  const vertical = orientation === "vertical";
17
- const diameter = theme.fontSize + 6;
18
+ const diameter = theme.indicatorSize;
18
19
  const rail = Math.max(4, Math.round(diameter / 3));
19
20
  const describedBy = [properties["aria-describedby"], description != null ? descriptionId : null].filter(Boolean).join(" ") || undefined;
20
21
  const controlled = value !== undefined;
@@ -31,7 +32,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, for
31
32
  if (!controlled)
32
33
  setUncontrolledValue(next);
33
34
  onChange?.(next);
34
- }, onValueCommitted: next => onChangeEnd?.(next), style: fieldStyle(theme, disabled, style), children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: theme.gap }, children: [label != null && _jsx(BaseSlider.Label, { style: { fontWeight: 550 }, children: label }), _jsx(BaseSlider.Value, { style: { fontVariantNumeric: "tabular-nums", opacity: 0.7 } })] }), _jsx(BaseSlider.Control, { style: {
35
+ }, onValueCommitted: next => onChangeEnd?.(next), style: fieldStyle(theme, disabled, style), children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: theme.gap }, children: [label != null && _jsx(BaseSlider.Label, { style: { fontWeight: controlFontWeight }, children: label }), _jsx(BaseSlider.Value, { style: { fontVariantNumeric: "tabular-nums", opacity: controlOpacity.secondary } })] }), _jsx(BaseSlider.Control, { style: {
35
36
  position: "relative",
36
37
  display: "flex",
37
38
  alignItems: "center",
@@ -39,7 +40,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, for
39
40
  touchAction: "none",
40
41
  userSelect: "none",
41
42
  width: vertical ? theme.height : "100%",
42
- height: vertical ? 160 : theme.height
43
+ height: vertical ? theme.height * 4 : theme.height
43
44
  }, children: _jsxs(BaseSlider.Track, { style: {
44
45
  position: "relative",
45
46
  width: vertical ? rail : "100%",
@@ -59,12 +60,12 @@ export const Slider = forwardRef(function Slider({ label, description, name, for
59
60
  borderRadius: "50%",
60
61
  boxSizing: "border-box",
61
62
  background: paint.color,
62
- border: `3px solid ${paint.background}`,
63
+ border: `${Math.max(1, diameter / 6)}px solid ${paint.background}`,
63
64
  outline: state.focused ? `1px solid ${theme.focusColor}` : "none",
64
65
  outlineOffset: 1,
65
66
  cursor: state.disabled ? "not-allowed" : state.dragging ? "grabbing" : "grab",
66
67
  transform: `scale(${state.dragging ? 0.92 : hovered ? 1.08 : 1})`
67
68
  };
68
- } })] }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
69
+ } })] }) }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: controlOpacity.secondary }, children: description })] });
69
70
  return _jsx(BaseDirectionProvider, { direction: direction, children: root });
70
71
  });
@@ -1,9 +1,11 @@
1
+ import { type CSSProperties } from "react";
1
2
  type EdgeMaterial = Readonly<{
2
3
  color: string;
3
4
  opacity: number;
4
5
  }>;
5
6
  /** Paints the boundary owned by one Surface from the material it contains. */
6
- export declare function SurfaceEdge({ material }: Readonly<{
7
+ export declare function SurfaceEdge({ material, transition }: Readonly<{
7
8
  material: EdgeMaterial;
9
+ transition: CSSProperties;
8
10
  }>): import("react").JSX.Element | null;
9
11
  export {};
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Fragment } from "react";
3
- import { useTransitionTiming } from "./motion-style.js";
4
3
  import { scale } from "./scale.js";
5
4
  const edgeLayer = {
6
5
  position: "absolute",
@@ -17,15 +16,14 @@ const illuminatedEdgeBoundary = Math.round((outerEdgeThickness + illuminatedEdge
17
16
  const horizontalIllumination = "linear-gradient(90deg, var(--phreshos-surface-edge-light-minimum) 0%, var(--phreshos-surface-edge-light-minimum) 12%, var(--phreshos-surface-edge-light-soft) 34%, var(--phreshos-surface-edge-light-peak) 50%, var(--phreshos-surface-edge-light-soft) 66%, var(--phreshos-surface-edge-light-minimum) 88%, var(--phreshos-surface-edge-light-minimum) 100%)";
18
17
  const edgeMask = "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)";
19
18
  /** Paints the boundary owned by one Surface from the material it contains. */
20
- export function SurfaceEdge({ material }) {
21
- const timing = useTransitionTiming();
19
+ export function SurfaceEdge({ material, transition }) {
22
20
  const visible = material.opacity > 0;
23
21
  if (!visible)
24
22
  return null;
25
23
  const opacity = Math.min(1, scale(material.opacity, "xlarge"));
26
24
  const peak = illumination(material.color, 92);
27
25
  return _jsxs(Fragment, { children: [_jsx("span", { "data-surface-edge-light": "", "aria-hidden": "true", style: {
28
- ...timing,
26
+ ...transition,
29
27
  ...edgeLayer,
30
28
  transitionProperty: "opacity",
31
29
  padding: illuminatedEdgeBoundary,
@@ -39,7 +37,7 @@ export function SurfaceEdge({ material }) {
39
37
  "--phreshos-surface-edge-light-soft": illumination(material.color, 56),
40
38
  "--phreshos-surface-edge-light-minimum": illumination(material.color, 24)
41
39
  } }), _jsx("span", { "data-surface-edge": "", "aria-hidden": "true", style: {
42
- ...timing,
40
+ ...transition,
43
41
  ...edgeLayer,
44
42
  transitionProperty: "border-color, opacity",
45
43
  borderStyle: "solid",
package/dist/surface.d.ts CHANGED
@@ -4,7 +4,7 @@ import { type MaterialOverrides } from "./material-options.js";
4
4
  import { type RadiusProps } from "./radius.js";
5
5
  import { type ShadowOverrides } from "./shadow-options.js";
6
6
  import type { Direction } from "./direction.js";
7
- export type { MaterialOptions } from "./material-options.js";
7
+ export type { MaterialMode, MaterialOptions } from "./material-options.js";
8
8
  export type { ShadowOptions } from "./shadow-options.js";
9
9
  export interface SurfaceOwnProps extends MaterialOverrides, ShadowOverrides, RadiusProps {
10
10
  readonly color?: Color;
package/dist/surface.js CHANGED
@@ -1,21 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement, forwardRef, useId } from "react";
3
- import { useAppearance, useThemedValue } from "./ui-provider.js";
4
- import { useResolveColor } from "./color.js";
5
- import { useMaterialOptions } from "./material-options.js";
3
+ import { useResolvedAppearance } from "./appearance-context.js";
4
+ import { contrastingColor, defaultColor, resolveColor } from "./color.js";
5
+ import { resolveMaterialOptions } from "./material-options.js";
6
6
  import { MaterialPaint } from "./material-paint.js";
7
- import MotionStyle, { useVisualTransition } from "./motion-style.js";
7
+ import { paintTransition, transitionTiming, visualTransition } from "./motion-style.js";
8
8
  import { resolveRadius } from "./radius.js";
9
- import { shadowStyle, useShadowOptions } from "./shadow-options.js";
9
+ import { resolveShadowOptions, shadowStyle } from "./shadow-options.js";
10
10
  import { SurfaceEdge } from "./surface-edge.js";
11
11
  /** One material- and shadow-owning element. The host is a div unless `as` selects another element. */
12
12
  const SurfaceRoot = forwardRef(function Surface({ as: Element = "div", color, material: options, shadow: shadowOptions, radius, children, style, ...properties }, ref) {
13
- const appearance = useAppearance();
14
- const transition = useVisualTransition();
15
- const material = useResolvedSurface(color, options);
16
- const resolvedShadow = useShadowOptions(typeof shadowOptions === "object" ? shadowOptions : undefined);
13
+ const resolved = useResolvedAppearance();
14
+ const transition = visualTransition(resolved.transaction, resolved.preferences.animations);
15
+ const material = resolveSurface(color, options, resolved);
16
+ const resolvedShadow = resolveShadowOptions(typeof shadowOptions === "object" ? shadowOptions : {}, resolved.shadow);
17
17
  const shadow = shadowOptions === false ? "none" : shadowStyle(resolvedShadow);
18
- const resolvedRadius = resolveRadius(radius ?? "medium", appearance);
18
+ const resolvedRadius = resolveRadius(radius ?? "medium", resolved.appearance);
19
+ const timing = transitionTiming(resolved.transaction, resolved.preferences.animations);
20
+ const paintTiming = paintTransition(resolved.transaction, resolved.preferences.animations);
19
21
  return createElement(Element, {
20
22
  ...properties,
21
23
  ref,
@@ -29,27 +31,46 @@ const SurfaceRoot = forwardRef(function Surface({ as: Element = "div", color, ma
29
31
  position: style?.position ?? "relative",
30
32
  isolation: "isolate"
31
33
  }
32
- }, material.enabled && _jsx(SurfaceLayers, { material: material }), material.enabled && _jsx(SurfaceEdge, { material: material }), children);
34
+ }, material.enabled && _jsx(SurfaceLayers, { material: material, paintTransition: paintTiming }), material.enabled && _jsx(SurfaceEdge, { material: material, transition: timing }), children);
33
35
  });
34
36
  export const Surface = SurfaceRoot;
35
- function useResolvedSurface(color, options) {
36
- const appearance = useAppearance();
37
- const material = useMaterialOptions(typeof options === "object" ? options : undefined);
37
+ function resolveSurface(color, options, resolved) {
38
+ const mode = typeof options === "object" ? "full" : options ?? "basic";
39
+ const level = materialLevel[mode];
40
+ const material = resolveMaterialOptions(typeof options === "object" ? options : {}, resolved.material);
41
+ const fill = resolveColor(color, resolved.colors);
42
+ const opacity = level >= materialLevel.extended ? material.opacity : 1;
43
+ const backdrop = level >= materialLevel.full;
44
+ const grain = level === materialLevel.basic
45
+ ? material.grain * material.opacity
46
+ : material.grain;
38
47
  return {
39
48
  ...material,
40
- enabled: options !== false,
41
- color: useResolveColor(color),
42
- foreground: useThemedValue(appearance.colors).foreground
49
+ enabled: level >= materialLevel.basic,
50
+ opacity,
51
+ fillOpacity: opacity,
52
+ grain,
53
+ backdrop: backdrop ? material.backdrop : 0,
54
+ distortion: backdrop ? material.distortion : 0,
55
+ saturation: backdrop ? material.saturation : 1,
56
+ color: fill,
57
+ foreground: contrastingColor(color ?? defaultColor, resolved.colors)
43
58
  };
44
59
  }
60
+ const materialLevel = Object.freeze({
61
+ none: 0,
62
+ basic: 1,
63
+ extended: 2,
64
+ full: 3
65
+ });
45
66
  const layerStyle = {
46
67
  position: "absolute",
47
68
  inset: 0,
48
69
  borderRadius: "inherit",
49
70
  pointerEvents: "none"
50
71
  };
51
- function SurfaceLayers({ material }) {
52
- const filtersVisible = material.opacity < 1;
72
+ function SurfaceLayers({ material, paintTransition }) {
73
+ const filtersVisible = material.fillOpacity < 1;
53
74
  const backdrop = filtersVisible ? material.backdrop : 0;
54
75
  const saturation = filtersVisible ? material.saturation : 1;
55
76
  const distortion = filtersVisible ? material.distortion : 0;
@@ -58,7 +79,7 @@ function SurfaceLayers({ material }) {
58
79
  saturation === 1 ? "" : `saturate(${saturation})`
59
80
  ].filter(Boolean).join(" ");
60
81
  const identity = `phresh-material-${useId().replaceAll(":", "")}`;
61
- 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 })] });
82
+ return _jsxs("span", { "data-material": "", "aria-hidden": "true", style: layerStyle, children: [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.fillOpacity, transition: paintTransition })] });
62
83
  }
63
84
  /** Keep refraction and native frost in independent compositor passes. */
64
85
  function BackdropLayer({ filter, name, zIndex }) {
@@ -1,13 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { motion } from "motion/react";
3
- import { useControlTransition } from "./motion-style.js";
4
- import { usePreferences } from "./ui-provider.js";
5
3
  import { Surface } from "./surface.js";
6
4
  /** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
7
5
  export function ToggleIndicator({ kind, direction = "ltr", selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options, shadow }) {
8
- const { animations } = usePreferences();
9
- const transition = useControlTransition();
10
- const diameter = theme.fontSize + 6;
6
+ const { animations, motionTransition: transition } = theme;
7
+ const diameter = theme.indicatorSize;
11
8
  const active = selected || indeterminate;
12
9
  const paints = invalid ? theme.paints.danger : active ? theme.paints.palette : theme.paints.neutral;
13
10
  const paint = pressed ? paints.pressed : hovered ? paints.hover : paints.rest;
package/dist/tooltip.js CHANGED
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
4
- import { useAppearance } from "./ui-provider.js";
4
+ import { useResolvedAppearance } from "./appearance-context.js";
5
5
  import { Button } from "./button.js";
6
6
  import { controlFontSizes } from "./control.js";
7
- import { overlayMotionClass, useOverlayTransition } from "./motion-style.js";
7
+ import MotionStyle, { overlayMotionClass, overlayTransition } from "./motion-style.js";
8
8
  import { scale } from "./scale.js";
9
9
  import { Surface } from "./surface.js";
10
10
  import { resolveDirection, useDirection } from "./direction.js";
@@ -16,19 +16,20 @@ export const TooltipTrigger = forwardRef(function TooltipTrigger(properties, ref
16
16
  return _jsx(Button, { ...properties, ref: ref });
17
17
  });
18
18
  export const TooltipContent = forwardRef(function TooltipContent({ children, className, color, material, radius, shadow, style, offset, placement = "top", ...properties }, ref) {
19
- const inset = scale(useAppearance().spacing, "small");
20
- const transition = useOverlayTransition();
19
+ const resolved = useResolvedAppearance();
20
+ const inset = scale(resolved.appearance.spacing, "small");
21
+ const transition = overlayTransition(resolved.transaction, resolved.preferences.animations);
21
22
  const direction = resolveDirection(properties.dir, useDirection());
22
- return _jsx(AriaTooltip, { ...properties, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
23
- boxSizing: "border-box",
24
- maxWidth: 280,
25
- paddingBlock: inset,
26
- paddingInline: inset * 1.5,
27
- outline: "none",
28
- fontSize: controlFontSizes.small,
29
- lineHeight: 1.4,
30
- ...style
31
- }, children: children }) });
23
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaTooltip, { ...properties, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
24
+ boxSizing: "border-box",
25
+ maxWidth: "28em",
26
+ paddingBlock: inset,
27
+ paddingInline: inset * 1.5,
28
+ outline: "none",
29
+ fontSize: controlFontSizes.small,
30
+ lineHeight: 1.4,
31
+ ...style
32
+ }, children: children }) })] });
32
33
  });
33
34
  /** A description shown from the focus and hover state of its trigger. */
34
35
  export const Tooltip = Object.assign(TooltipRoot, {
@@ -1,20 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AppearanceContext, PreferencesContext } from "./appearance-context.js";
3
- import { DirectionContext } from "./direction.js";
2
+ import { useMemo } from "react";
3
+ import { AppearanceContext, fixedPreferencesSource, PreferencesContext } from "./appearance-context.js";
4
+ import { DirectionContext, fixedDirectionSource } from "./direction.js";
4
5
  export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
5
6
  export { useDirection, useDocumentDirection } from "./direction.js";
6
7
  const directionBoundaryStyle = { display: "contents" };
7
8
  /** Establishes only the explicitly supplied UI values for a React subtree. */
8
9
  export function UIProvider({ appearance, children, direction, preferences }) {
9
10
  let subtree = children;
10
- if (preferences !== undefined) {
11
- subtree = _jsx(PreferencesContext.Provider, { value: preferences, children: subtree });
11
+ const preferencesSource = useMemo(() => preferences === undefined ? undefined : fixedPreferencesSource(preferences), [preferences]);
12
+ const directionSource = useMemo(() => direction === undefined ? undefined : fixedDirectionSource(direction), [direction]);
13
+ if (preferencesSource !== undefined) {
14
+ subtree = _jsx(PreferencesContext.Provider, { value: preferencesSource, children: subtree });
12
15
  }
13
16
  if (appearance !== undefined) {
14
17
  subtree = _jsx(AppearanceContext.Provider, { value: appearance, children: subtree });
15
18
  }
16
- if (direction !== undefined) {
17
- subtree = _jsx(DirectionContext.Provider, { value: direction, children: _jsx("div", { dir: direction, style: directionBoundaryStyle, children: subtree }) });
19
+ if (directionSource !== undefined) {
20
+ subtree = _jsx(DirectionContext.Provider, { value: directionSource, children: _jsx("div", { dir: direction, style: directionBoundaryStyle, children: subtree }) });
18
21
  }
19
22
  return subtree;
20
23
  }
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { type ButtonProps } from "./button.js";
3
+ import { type SurfaceProps } from "./surface.js";
4
+ /** The material shell that establishes the complete header-and-content layout. */
5
+ export type WindowProps = SurfaceProps;
6
+ /** The remaining window area. Its consumer owns content behavior, including overflow. */
7
+ export type WindowContentProps = HTMLAttributes<HTMLDivElement>;
8
+ export interface WindowHeaderProps extends HTMLAttributes<HTMLDivElement> {
9
+ /** Attenuates the identity when this header is not active. */
10
+ readonly active?: boolean;
11
+ }
12
+ export interface WindowIdentityProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
13
+ readonly icon?: string;
14
+ readonly title?: ReactNode;
15
+ }
16
+ export type WindowCenterProps = HTMLAttributes<HTMLDivElement>;
17
+ export type WindowActionsProps = HTMLAttributes<HTMLDivElement>;
18
+ export type WindowActionProps = Omit<ButtonProps, "size">;
19
+ export type WindowControlProps = Omit<WindowActionProps, "children">;
20
+ export interface WindowMaximizeProps extends WindowControlProps {
21
+ readonly maximized?: boolean;
22
+ }
23
+ export type WindowCloseProps = Omit<WindowControlProps, "color">;
24
+ /** One Surface with a ready-to-use header-and-content composition. */
25
+ export declare const Window: import("react").ForwardRefExoticComponent<Omit<WindowProps, "ref"> & import("react").RefAttributes<HTMLDivElement>> & {
26
+ Header: import("react").ForwardRefExoticComponent<WindowHeaderProps & import("react").RefAttributes<HTMLDivElement>> & {
27
+ Identity: import("react").ForwardRefExoticComponent<WindowIdentityProps & import("react").RefAttributes<HTMLDivElement>>;
28
+ Center: import("react").ForwardRefExoticComponent<WindowCenterProps & import("react").RefAttributes<HTMLDivElement>>;
29
+ Actions: import("react").ForwardRefExoticComponent<WindowActionsProps & import("react").RefAttributes<HTMLDivElement>>;
30
+ Action: import("react").ForwardRefExoticComponent<WindowActionProps & import("react").RefAttributes<HTMLButtonElement>>;
31
+ Minimize: import("react").ForwardRefExoticComponent<WindowControlProps & import("react").RefAttributes<HTMLButtonElement>>;
32
+ Maximize: import("react").ForwardRefExoticComponent<WindowMaximizeProps & import("react").RefAttributes<HTMLButtonElement>>;
33
+ Close: import("react").ForwardRefExoticComponent<WindowCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
34
+ };
35
+ Content: import("react").ForwardRefExoticComponent<WindowContentProps & import("react").RefAttributes<HTMLDivElement>>;
36
+ };