@phreshos/react-ui 0.1.29 → 0.1.31

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.
Files changed (84) hide show
  1. package/README.md +197 -39
  2. package/dist/accordion.d.ts +45 -0
  3. package/dist/accordion.js +62 -0
  4. package/dist/alert-dialog.d.ts +0 -1
  5. package/dist/alert-dialog.js +0 -1
  6. package/dist/aria-direction.d.ts +9 -0
  7. package/dist/aria-direction.js +49 -0
  8. package/dist/calendar-layout.d.ts +8 -0
  9. package/dist/calendar-layout.js +103 -0
  10. package/dist/calendar-trigger.d.ts +5 -0
  11. package/dist/calendar-trigger.js +30 -0
  12. package/dist/calendar.d.ts +18 -0
  13. package/dist/calendar.js +13 -0
  14. package/dist/color.d.ts +16 -1
  15. package/dist/color.js +60 -11
  16. package/dist/combo-box.d.ts +20 -0
  17. package/dist/combo-box.js +68 -0
  18. package/dist/context-menu.d.ts +2 -4
  19. package/dist/context-menu.js +0 -1
  20. package/dist/control-surface.d.ts +2 -1
  21. package/dist/control-surface.js +2 -2
  22. package/dist/control.d.ts +1 -1
  23. package/dist/control.js +7 -8
  24. package/dist/date-control.d.ts +24 -0
  25. package/dist/date-control.js +69 -0
  26. package/dist/date-field.d.ts +19 -0
  27. package/dist/date-field.js +13 -0
  28. package/dist/date-picker.d.ts +19 -0
  29. package/dist/date-picker.js +16 -0
  30. package/dist/date-range-picker.d.ts +20 -0
  31. package/dist/date-range-picker.js +25 -0
  32. package/dist/date-range.d.ts +6 -0
  33. package/dist/date-range.js +0 -0
  34. package/dist/dialog.d.ts +4 -2
  35. package/dist/dialog.js +24 -7
  36. package/dist/disclosure.d.ts +42 -0
  37. package/dist/disclosure.js +97 -0
  38. package/dist/dropdown-menu.d.ts +0 -2
  39. package/dist/dropdown-menu.js +0 -1
  40. package/dist/list-box.d.ts +49 -0
  41. package/dist/list-box.js +129 -0
  42. package/dist/main.d.ts +27 -11
  43. package/dist/main.js +26 -10
  44. package/dist/material-options.d.ts +4 -2
  45. package/dist/material-paint.js +19 -1
  46. package/dist/menu.d.ts +22 -4
  47. package/dist/menu.js +25 -6
  48. package/dist/panel.d.ts +0 -1
  49. package/dist/panel.js +0 -1
  50. package/dist/popover.d.ts +0 -1
  51. package/dist/popover.js +13 -3
  52. package/dist/progress-bar.d.ts +22 -0
  53. package/dist/progress-bar.js +46 -0
  54. package/dist/radio.d.ts +5 -5
  55. package/dist/radio.js +8 -4
  56. package/dist/radius.js +1 -1
  57. package/dist/range-calendar.d.ts +19 -0
  58. package/dist/range-calendar.js +15 -0
  59. package/dist/scroll-area.d.ts +4 -1
  60. package/dist/scroll-area.js +3 -3
  61. package/dist/select.js +4 -14
  62. package/dist/selection.d.ts +18 -0
  63. package/dist/selection.js +28 -0
  64. package/dist/slider.js +1 -1
  65. package/dist/spacing.js +1 -1
  66. package/dist/surface.d.ts +1 -1
  67. package/dist/surface.js +22 -3
  68. package/dist/table.d.ts +78 -0
  69. package/dist/table.js +148 -0
  70. package/dist/tabs.d.ts +45 -0
  71. package/dist/tabs.js +103 -0
  72. package/dist/time-field.d.ts +22 -0
  73. package/dist/time-field.js +13 -0
  74. package/dist/toggle-indicator.js +1 -1
  75. package/dist/toolbar.d.ts +46 -0
  76. package/dist/toolbar.js +121 -0
  77. package/dist/tooltip.d.ts +0 -1
  78. package/dist/tooltip.js +0 -1
  79. package/dist/tree.d.ts +56 -0
  80. package/dist/tree.js +134 -0
  81. package/dist/window.d.ts +39 -0
  82. package/dist/{window-header.js → window.js} +57 -31
  83. package/package.json +4 -2
  84. package/dist/window-header.d.ts +0 -42
@@ -43,12 +43,28 @@ function DistortionFilter({ distortion, identity }) {
43
43
  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" })] });
44
44
  }
45
45
  function grainImage(seed, amount) {
46
+ const key = `${seed}:${amount}`;
47
+ const cached = grainResources.get(key);
48
+ if (cached !== undefined) {
49
+ grainResources.delete(key);
50
+ grainResources.set(key, cached);
51
+ return cached;
52
+ }
46
53
  const paths = grainPaths(seed, amount)
47
54
  .map((path, tone) => path
48
55
  ? `<path d="${path}" fill="rgb(${toneChannel(tone)} ${toneChannel(tone)} ${toneChannel(tone)})"/>`
49
56
  : "")
50
57
  .join("");
51
- return `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${patternSize}" height="${patternSize}" viewBox="0 0 ${patternSize} ${patternSize}">${paths}</svg>`)}")`;
58
+ const resource = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${patternSize}" height="${patternSize}" viewBox="0 0 ${patternSize} ${patternSize}">${paths}</svg>`)}")`;
59
+ // Preserve the exact texture while avoiding the same expensive SVG generation
60
+ // for every Surface. The bound prevents live grain controls from growing it forever.
61
+ if (grainResources.size >= grainResourceLimit) {
62
+ const oldest = grainResources.keys().next().value;
63
+ if (oldest !== undefined)
64
+ grainResources.delete(oldest);
65
+ }
66
+ grainResources.set(key, resource);
67
+ return resource;
52
68
  }
53
69
  /** Preserve the original contract: amount is the monotonic density of visible grain pixels. */
54
70
  function grainPaths(seed, amount) {
@@ -99,3 +115,5 @@ function clamp(value, minimum, maximum) {
99
115
  const patternSize = 64;
100
116
  const toneCount = 16;
101
117
  const grainSeeds = [19, 47, 83, 131];
118
+ const grainResourceLimit = 64;
119
+ const grainResources = new Map();
package/dist/menu.d.ts CHANGED
@@ -2,18 +2,36 @@ import type { CSSProperties, ReactElement, RefAttributes } from "react";
2
2
  import type { HeaderProps as AriaHeaderProps, MenuItemProps as AriaMenuItemProps, MenuProps as AriaMenuProps, MenuSectionProps as AriaMenuSectionProps, SeparatorProps as AriaSeparatorProps } from "react-aria-components";
3
3
  import { type ControlColor } from "./control.js";
4
4
  import type { ScaleLevel } from "./scale.js";
5
- export interface MenuRootProps<T extends object = object> extends Omit<AriaMenuProps<T>, "className" | "style"> {
5
+ import { type MultipleStringSelection, type MultipleStringSelectionProps, type SingleStringSelectionProps } from "./selection.js";
6
+ type MenuRootBaseProps<T extends object> = Omit<AriaMenuProps<T>, "className" | "defaultSelectedKeys" | "disabledKeys" | "onAction" | "onSelectionChange" | "selectedKeys" | "selectionMode" | "style"> & Readonly<{
6
7
  readonly className?: string;
7
8
  readonly color?: ControlColor;
9
+ readonly disabledValues?: readonly string[];
10
+ readonly onItemAction?: (value: string) => void;
8
11
  readonly size?: ScaleLevel;
9
12
  readonly style?: CSSProperties;
10
- }
13
+ }>;
14
+ export type MenuNoSelectionProps = Readonly<{
15
+ selectionMode?: "none";
16
+ value?: never;
17
+ defaultValue?: never;
18
+ onChange?: never;
19
+ }>;
20
+ export type MenuSingleSelectionProps = SingleStringSelectionProps & Readonly<{
21
+ selectionMode: "single";
22
+ }>;
23
+ export type MenuMultipleValue = MultipleStringSelection;
24
+ export type MenuMultipleSelectionProps = MultipleStringSelectionProps & Readonly<{
25
+ selectionMode: "multiple";
26
+ }>;
27
+ export type MenuRootProps<T extends object = object> = MenuRootBaseProps<T> & (MenuNoSelectionProps | MenuSingleSelectionProps | MenuMultipleSelectionProps);
11
28
  export type MenuRootComponent = <T extends object = object>(properties: MenuRootProps<T> & RefAttributes<HTMLDivElement>) => ReactElement | null;
12
29
  export declare const MenuRoot: MenuRootComponent;
13
- export interface MenuItemProps<T = object> extends Omit<AriaMenuItemProps<T>, "className" | "isDisabled" | "style"> {
30
+ export interface MenuItemProps<T = object> extends Omit<AriaMenuItemProps<T>, "className" | "id" | "isDisabled" | "style"> {
14
31
  readonly className?: string;
15
32
  readonly color?: ControlColor;
16
33
  readonly disabled?: boolean;
34
+ readonly id?: string;
17
35
  readonly style?: CSSProperties;
18
36
  }
19
37
  export type MenuItemComponent = <T = object>(properties: MenuItemProps<T> & RefAttributes<HTMLDivElement>) => ReactElement | null;
@@ -26,10 +44,10 @@ export type MenuSeparatorProps = AriaSeparatorProps;
26
44
  export declare const MenuSeparator: import("react").ForwardRefExoticComponent<AriaSeparatorProps & RefAttributes<HTMLElement>>;
27
45
  /** A keyboard-navigable collection of commands or selectable options. */
28
46
  export declare const Menu: MenuRootComponent & {
29
- Root: MenuRootComponent;
30
47
  Item: MenuItemComponent;
31
48
  Section: <T>(props: AriaMenuSectionProps<T> & React.RefAttributes<HTMLElement>) => ReactElement<unknown, string | React.JSXElementConstructor<any>> | null;
32
49
  Header: import("react").ForwardRefExoticComponent<AriaHeaderProps & RefAttributes<HTMLElement>>;
33
50
  Separator: import("react").ForwardRefExoticComponent<AriaSeparatorProps & RefAttributes<HTMLElement>>;
34
51
  };
35
52
  export type MenuProps<T extends object = object> = MenuRootProps<T>;
53
+ export {};
package/dist/menu.js CHANGED
@@ -3,23 +3,43 @@ import { createContext, forwardRef, useContext, useMemo } from "react";
3
3
  import { Header as AriaHeader, Menu as AriaMenu, MenuItem as AriaMenuItem, MenuSection as AriaMenuSection, Separator as AriaSeparator } from "react-aria-components";
4
4
  import { colorOpacity } from "./color.js";
5
5
  import { controlFontWeight, controlOpacity, useControlTheme } from "./control.js";
6
+ import { multipleSelection, singleSelection, stringKey, toAriaSelection } from "./selection.js";
6
7
  const MenuThemeContext = createContext(null);
7
- const MenuRootImplementation = forwardRef(function MenuRoot({ className, color, size = "medium", style, ...properties }, ref) {
8
+ const MenuRootImplementation = forwardRef(function MenuRoot(properties, ref) {
9
+ const { className, color, defaultValue, disabledValues, onChange, onItemAction, selectionMode = "none", size = "medium", style, value, ...native } = properties;
8
10
  const theme = useControlTheme({ color, size });
9
11
  const context = useMemo(() => ({ color, size, theme }), [color, size, theme]);
10
- return _jsx(MenuThemeContext.Provider, { value: context, children: _jsx(AriaMenu, { ...properties, ref: ref, className: className, style: {
12
+ return _jsx(MenuThemeContext.Provider, { value: context, children: _jsx(AriaMenu, { ...native, ...selectionProperties(properties), ref: ref, className: className, disabledKeys: disabledValues, selectionMode: selectionMode === "none" ? undefined : selectionMode, onAction: onItemAction == null ? undefined : key => onItemAction(stringKey(key)), style: {
11
13
  display: "grid",
12
14
  gap: theme.gap,
13
15
  minWidth: theme.height * 4,
14
16
  padding: theme.gap,
15
17
  boxSizing: "border-box",
16
18
  outline: "none",
17
- color: theme.foreground,
18
19
  fontFamily: "inherit",
19
20
  fontSize: theme.fontSize,
20
21
  ...style
21
22
  } }) });
22
23
  });
24
+ function selectionProperties(properties) {
25
+ const selectedKeys = toAriaSelection(properties.value);
26
+ const defaultSelectedKeys = toAriaSelection(properties.defaultValue);
27
+ const onSelectionChange = properties.onChange == null
28
+ ? undefined
29
+ : (selection) => {
30
+ if (properties.selectionMode === "multiple") {
31
+ properties.onChange?.(multipleSelection(selection));
32
+ return;
33
+ }
34
+ if (properties.selectionMode === "single")
35
+ properties.onChange?.(singleSelection(selection));
36
+ };
37
+ return {
38
+ ...(selectedKeys !== undefined ? { selectedKeys } : {}),
39
+ ...(defaultSelectedKeys !== undefined ? { defaultSelectedKeys } : {}),
40
+ ...(onSelectionChange !== undefined ? { onSelectionChange } : {})
41
+ };
42
+ }
23
43
  export const MenuRoot = MenuRootImplementation;
24
44
  const MenuItemImplementation = forwardRef(function MenuItem({ className, color, disabled = false, style, ...properties }, ref) {
25
45
  const inherited = useContext(MenuThemeContext);
@@ -37,7 +57,7 @@ function MenuItemView({ className, disabled, properties, ref, style, theme }) {
37
57
  ? theme.paints.palette.pressed
38
58
  : state.isHovered || state.isSelected
39
59
  ? theme.paints.palette.hover
40
- : { background: "transparent", color: theme.foreground };
60
+ : { background: "transparent", color: "inherit" };
41
61
  return {
42
62
  ...theme.transition,
43
63
  display: "flex",
@@ -77,7 +97,7 @@ export const MenuSeparator = forwardRef(function MenuSeparator({ style, ...prope
77
97
  height: 1,
78
98
  marginBlock: theme.gap,
79
99
  border: 0,
80
- background: colorOpacity(theme.foreground, controlOpacity.separator),
100
+ background: colorOpacity(theme.tint, controlOpacity.separator),
81
101
  ...style
82
102
  } });
83
103
  });
@@ -89,7 +109,6 @@ function useMenuTheme() {
89
109
  }
90
110
  /** A keyboard-navigable collection of commands or selectable options. */
91
111
  export const Menu = Object.assign(MenuRoot, {
92
- Root: MenuRoot,
93
112
  Item: MenuItem,
94
113
  Section: MenuSection,
95
114
  Header: MenuHeader,
package/dist/panel.d.ts CHANGED
@@ -11,7 +11,6 @@ export type PanelContentProps = SurfaceProps;
11
11
  export declare const PanelContent: import("react").ForwardRefExoticComponent<Omit<PanelContentProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
12
12
  /** A material shell whose named parts remain independently composable. */
13
13
  export declare const Panel: import("react").ForwardRefExoticComponent<Omit<PanelRootProps, "ref"> & import("react").RefAttributes<HTMLDivElement>> & {
14
- Root: import("react").ForwardRefExoticComponent<Omit<PanelRootProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
15
14
  Header: import("react").ForwardRefExoticComponent<PanelHeaderProps & import("react").RefAttributes<HTMLDivElement>>;
16
15
  Content: import("react").ForwardRefExoticComponent<Omit<PanelContentProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
17
16
  };
package/dist/panel.js CHANGED
@@ -36,7 +36,6 @@ export const PanelContent = forwardRef(function PanelContent({ style, ...propert
36
36
  });
37
37
  /** A material shell whose named parts remain independently composable. */
38
38
  export const Panel = Object.assign(PanelRoot, {
39
- Root: PanelRoot,
40
39
  Header: PanelHeader,
41
40
  Content: PanelContent
42
41
  });
package/dist/popover.d.ts CHANGED
@@ -20,7 +20,6 @@ export type PopoverCloseProps = ButtonProps;
20
20
  export declare const PopoverClose: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
21
21
  /** An anchored non-modal overlay with explicit trigger, surface, and dialog roles. */
22
22
  export declare const Popover: typeof PopoverRoot & {
23
- Root: typeof PopoverRoot;
24
23
  Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
25
24
  Content: import("react").ForwardRefExoticComponent<PopoverContentProps & import("react").RefAttributes<HTMLElement>>;
26
25
  Dialog: import("react").ForwardRefExoticComponent<AriaDialogProps & import("react").RefAttributes<HTMLElement>>;
package/dist/popover.js CHANGED
@@ -3,6 +3,7 @@ import { forwardRef } from "react";
3
3
  import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Heading as AriaHeading, Popover as AriaPopover } from "react-aria-components";
4
4
  import { useResolvedAppearance } from "./appearance-context.js";
5
5
  import { Button } from "./button.js";
6
+ import { controlFontSizes, controlFontWeight } from "./control.js";
6
7
  import MotionStyle, { overlayMotionClass, overlayTransition } from "./motion-style.js";
7
8
  import { scale } from "./scale.js";
8
9
  import { Surface } from "./surface.js";
@@ -30,15 +31,24 @@ export const PopoverContent = forwardRef(function PopoverContent({ children, cla
30
31
  export const PopoverDialog = forwardRef(function PopoverDialog({ style, ...properties }, ref) {
31
32
  return _jsx(AriaDialog, { ...properties, ref: ref, style: { minWidth: 0, outline: "none", ...style } });
32
33
  });
33
- export const PopoverTitle = forwardRef(function PopoverTitle(properties, ref) {
34
- return _jsx(AriaHeading, { ...properties, ref: ref, slot: "title" });
34
+ export const PopoverTitle = forwardRef(function PopoverTitle({ style, ...properties }, ref) {
35
+ return _jsx(AriaHeading, { ...properties, ref: ref, slot: "title",
36
+ // Portalled headings cannot rely on the trigger subtree to neutralize
37
+ // browser heading defaults; this part owns its complete text treatment.
38
+ style: {
39
+ margin: 0,
40
+ fontFamily: "inherit",
41
+ fontSize: controlFontSizes.medium,
42
+ fontWeight: controlFontWeight,
43
+ lineHeight: 1.5,
44
+ ...style
45
+ } });
35
46
  });
36
47
  export const PopoverClose = forwardRef(function PopoverClose(properties, ref) {
37
48
  return _jsx(Button, { ...properties, ref: ref, slot: "close" });
38
49
  });
39
50
  /** An anchored non-modal overlay with explicit trigger, surface, and dialog roles. */
40
51
  export const Popover = Object.assign(PopoverRoot, {
41
- Root: PopoverRoot,
42
52
  Trigger: PopoverTrigger,
43
53
  Content: PopoverContent,
44
54
  Dialog: PopoverDialog,
@@ -0,0 +1,22 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { ProgressBarProps as AriaProgressBarProps } from "react-aria-components";
3
+ import type { ControlColor } from "./control.js";
4
+ import type { ScaleLevel } from "./scale.js";
5
+ type NativeProgressBarProps = Omit<AriaProgressBarProps, "children" | "className" | "isIndeterminate" | "style">;
6
+ export interface ProgressBarProps extends NativeProgressBarProps {
7
+ /** Native class name applied without replacing the component contract. */
8
+ readonly className?: string;
9
+ /** Color used for the completed or moving portion. */
10
+ readonly color?: ControlColor;
11
+ /** Displays progress without asserting a current value. */
12
+ readonly indeterminate?: boolean;
13
+ /** Visible label associated with the progress value. */
14
+ readonly label?: ReactNode;
15
+ /** Derives typography and track thickness from Appearance. */
16
+ readonly size?: ScaleLevel;
17
+ /** Native styles applied after ProgressBar defaults. */
18
+ readonly style?: CSSProperties;
19
+ }
20
+ /** Determinate or indeterminate progress for an operation over time. */
21
+ export declare const ProgressBar: import("react").ForwardRefExoticComponent<ProgressBarProps & import("react").RefAttributes<HTMLDivElement>>;
22
+ export {};
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { Label, ProgressBar as AriaProgressBar } from "react-aria-components";
4
+ import { motion } from "motion/react";
5
+ import { controlFontWeight, controlOpacity, fieldStyle, useControlTheme } from "./control.js";
6
+ import { resolveDirection, useDirection } from "./direction.js";
7
+ /** Determinate or indeterminate progress for an operation over time. */
8
+ export const ProgressBar = forwardRef(function ProgressBar({ className, color = "default:base", indeterminate = false, label, size = "medium", style, value, minValue = 0, maxValue = 100, ...properties }, ref) {
9
+ const theme = useControlTheme({ color, size });
10
+ const direction = resolveDirection(properties.dir, useDirection());
11
+ const thickness = Math.max(2, Math.round(theme.spacing / 2));
12
+ const sweep = direction === "rtl" ? ["100%", "-350%"] : ["-100%", "350%"];
13
+ const duration = typeof theme.motionTransition.duration === "number"
14
+ ? theme.motionTransition.duration * 10
15
+ : 1.2;
16
+ return _jsx(AriaProgressBar, { ...properties, ref: ref, className: className, value: value, minValue: minValue, maxValue: maxValue, isIndeterminate: indeterminate, style: {
17
+ ...fieldStyle(theme, false),
18
+ width: "100%",
19
+ ...style
20
+ }, children: state => {
21
+ const output = properties.valueLabel ?? state.valueText;
22
+ const showHeading = label != null || (!state.isIndeterminate && output != null);
23
+ const moving = state.isIndeterminate && theme.animations;
24
+ return _jsxs(_Fragment, { children: [showHeading && _jsxs("span", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: theme.gap }, children: [label != null && _jsx(Label, { style: { fontWeight: controlFontWeight }, children: label }), !state.isIndeterminate && output != null && _jsx("span", { style: { fontVariantNumeric: "tabular-nums", opacity: controlOpacity.secondary }, children: output })] }), _jsx("span", { "data-progress-track": "", "aria-hidden": "true", style: {
25
+ position: "relative",
26
+ display: "block",
27
+ width: "100%",
28
+ height: thickness,
29
+ overflow: "hidden",
30
+ borderRadius: thickness,
31
+ background: theme.paints.subtle.rest.background
32
+ }, children: _jsx(motion.span, { "data-progress-fill": "", initial: false, animate: { x: moving ? sweep : "0%" }, transition: moving
33
+ ? { ...theme.motionTransition, duration, repeat: Infinity }
34
+ : theme.motionTransition, style: {
35
+ ...theme.transition,
36
+ position: "absolute",
37
+ insetBlock: 0,
38
+ insetInlineStart: state.isIndeterminate && !moving ? "30%" : 0,
39
+ display: "block",
40
+ width: state.isIndeterminate ? "40%" : `${state.percentage ?? 0}%`,
41
+ borderRadius: "inherit",
42
+ background: theme.paints.palette.rest.background,
43
+ transitionProperty: "width"
44
+ } }) })] });
45
+ } });
46
+ });
package/dist/radio.d.ts CHANGED
@@ -12,10 +12,10 @@ export interface RadioGroupProps extends Omit<BaseRadioGroupProps<string | null>
12
12
  readonly readOnly?: boolean;
13
13
  readonly orientation?: "horizontal" | "vertical";
14
14
  }
15
- export interface RadioProps extends Omit<BaseRadioProps<string>, "children" | "className" | "disabled" | "readOnly" | "required" | "style" | "value">, ControlProps, Pick<FieldProps, "label" | "description">, MaterialOverrides, ShadowOverrides {
15
+ export interface RadioGroupItemProps extends Omit<BaseRadioProps<string>, "children" | "className" | "disabled" | "readOnly" | "required" | "style" | "value">, ControlProps, Pick<FieldProps, "label" | "description">, MaterialOverrides, ShadowOverrides {
16
16
  readonly value: string;
17
17
  }
18
- /** One string value selected from its Radio children, with direction-aware arrow navigation. */
19
- export declare const RadioGroup: import("react").ForwardRefExoticComponent<Omit<RadioGroupProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
20
- /** An option in a RadioGroup. Selection and validation belong to the group. */
21
- export declare const Radio: import("react").ForwardRefExoticComponent<Omit<RadioProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
18
+ /** One exclusive-choice field whose context-dependent options are grouped as Items. */
19
+ export declare const RadioGroup: import("react").ForwardRefExoticComponent<Omit<RadioGroupProps, "ref"> & import("react").RefAttributes<HTMLDivElement>> & {
20
+ Item: import("react").ForwardRefExoticComponent<Omit<RadioGroupItemProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
21
+ };
package/dist/radio.js CHANGED
@@ -8,8 +8,8 @@ import { controlFontWeight, controlOpacity, fieldStyle, useControlTheme } from "
8
8
  import { ToggleIndicator, toggleStyle } from "./toggle-indicator.js";
9
9
  import { resolveDirection, useDirection } from "./direction.js";
10
10
  const RadioStyleContext = createContext(null);
11
- /** One string value selected from its Radio children, with direction-aware arrow navigation. */
12
- export const RadioGroup = forwardRef(function RadioGroup({ label, description, errorMessage, children, disabled = false, readOnly = false, required = false, invalid = false, value, defaultValue, onChange, orientation = "vertical", size, color, style, className, material, shadow, "aria-describedby": ariaDescribedBy, "aria-labelledby": ariaLabelledBy, ...properties }, ref) {
11
+ /** One string value selected from its Items, with direction-aware arrow navigation. */
12
+ const RadioGroupRoot = forwardRef(function RadioGroupRoot({ label, description, errorMessage, children, disabled = false, readOnly = false, required = false, invalid = false, value, defaultValue, onChange, orientation = "vertical", size, color, style, className, material, shadow, "aria-describedby": ariaDescribedBy, "aria-labelledby": ariaLabelledBy, ...properties }, ref) {
13
13
  const theme = useControlTheme({ size, color });
14
14
  const direction = resolveDirection(properties.dir, useDirection());
15
15
  const labelId = useId();
@@ -23,10 +23,10 @@ export const RadioGroup = forwardRef(function RadioGroup({ label, description, e
23
23
  return _jsx(BaseDirectionProvider, { direction: direction, children: root });
24
24
  });
25
25
  /** An option in a RadioGroup. Selection and validation belong to the group. */
26
- export const Radio = forwardRef(function Radio({ label, description, value, disabled = false, size, color, style, className, material, shadow, onFocus, onBlur, "aria-describedby": ariaDescribedBy, ...properties }, ref) {
26
+ const RadioGroupItem = forwardRef(function RadioGroupItem({ label, description, value, disabled = false, size, color, style, className, material, shadow, onFocus, onBlur, "aria-describedby": ariaDescribedBy, ...properties }, ref) {
27
27
  const inherited = useContext(RadioStyleContext);
28
28
  if (inherited == null)
29
- throw new Error("Radio must be used inside RadioGroup");
29
+ throw new Error("RadioGroup.Item must be used inside RadioGroup");
30
30
  const theme = useControlTheme({ size: size ?? inherited.size, color: color ?? inherited.color });
31
31
  const descriptionId = useId();
32
32
  const [hovered, setHovered] = useState(false);
@@ -41,3 +41,7 @@ export const Radio = forwardRef(function Radio({ label, description, value, disa
41
41
  onBlur?.(event);
42
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 })] });
43
43
  });
44
+ /** One exclusive-choice field whose context-dependent options are grouped as Items. */
45
+ export const RadioGroup = Object.assign(RadioGroupRoot, {
46
+ Item: RadioGroupItem
47
+ });
package/dist/radius.js CHANGED
@@ -6,6 +6,6 @@ export function resolveRadius(value, appearance) {
6
6
  if (!isScaleLevel(value))
7
7
  return value;
8
8
  if (!appearance)
9
- throw new Error("A semantic radius requires a UIProvider");
9
+ throw new Error("A semantic radius requires an Appearance value");
10
10
  return scale(appearance.radius, value);
11
11
  }
@@ -0,0 +1,19 @@
1
+ import type { CalendarDate } from "@internationalized/date";
2
+ import type { RangeCalendarProps as AriaRangeCalendarProps } from "react-aria-components";
3
+ import { type ControlOverrides, type ControlProps } from "./control.js";
4
+ import type { DateRange } from "./date-range.js";
5
+ import type { RadiusProps } from "./radius.js";
6
+ type RangeCalendarOverrides = ControlOverrides | "children" | "isReadOnly" | "isInvalid" | "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isDateUnavailable" | "visibleDuration" | "createCalendar" | "errorMessage" | "validationState";
7
+ export interface RangeCalendarProps extends Omit<AriaRangeCalendarProps<CalendarDate>, RangeCalendarOverrides>, ControlProps, RadiusProps {
8
+ readonly value?: DateRange | null;
9
+ readonly defaultValue?: DateRange | null;
10
+ readonly onChange?: (value: DateRange) => void;
11
+ readonly minValue?: CalendarDate | null;
12
+ readonly maxValue?: CalendarDate | null;
13
+ readonly isDateUnavailable?: (date: CalendarDate, anchorDate: CalendarDate | null) => boolean;
14
+ readonly readOnly?: boolean;
15
+ readonly invalid?: boolean;
16
+ }
17
+ /** A locale-aware month view for selecting an inclusive date range. */
18
+ export declare const RangeCalendar: import("react").ForwardRefExoticComponent<RangeCalendarProps & import("react").RefAttributes<HTMLDivElement>>;
19
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { RangeCalendar as AriaRangeCalendar } from "react-aria-components";
4
+ import { AriaDirectionBoundary } from "./aria-direction.js";
5
+ import { CalendarLayout, calendarRootStyle } from "./calendar-layout.js";
6
+ import { useControlTheme } from "./control.js";
7
+ import { resolveDirection, useDirection } from "./direction.js";
8
+ /** A locale-aware month view for selecting an inclusive date range. */
9
+ export const RangeCalendar = forwardRef(function RangeCalendar({ disabled, readOnly, invalid, size, color, radius, style, className, isDateUnavailable, ...properties }, ref) {
10
+ const theme = useControlTheme({ size, color, radius });
11
+ const direction = resolveDirection(properties.dir, useDirection());
12
+ return _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaRangeCalendar, { ...properties, ref: ref, dir: direction, className: className, isDisabled: disabled, isReadOnly: readOnly, isInvalid: invalid, isDateUnavailable: isDateUnavailable == null
13
+ ? undefined
14
+ : (date, anchorDate) => isDateUnavailable(date, anchorDate), style: calendarRootStyle(theme, disabled, style), children: _jsx(CalendarLayout, { color: color, range: true, size: size, theme: theme }) }) });
15
+ });
@@ -1,8 +1,11 @@
1
1
  import type { ComponentPropsWithoutRef, Ref, ReactNode, UIEventHandler } from "react";
2
+ import type { ControlColor } from "./control.js";
2
3
  export type ScrollAreaAxis = "vertical" | "horizontal" | "both";
3
- export interface ScrollAreaProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onScroll"> {
4
+ export interface ScrollAreaProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "color" | "onScroll"> {
4
5
  readonly axis?: ScrollAreaAxis;
5
6
  readonly children?: ReactNode;
7
+ /** Base color from which scrollbar states are derived. */
8
+ readonly color?: ControlColor;
6
9
  readonly onScroll?: UIEventHandler<HTMLDivElement>;
7
10
  readonly viewportRef?: Ref<HTMLDivElement>;
8
11
  }
@@ -3,15 +3,15 @@ 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
5
  import { useResolvedAppearance } from "./appearance-context.js";
6
- import { colorOpacity } from "./color.js";
6
+ import { colorOpacity, resolveColor } from "./color.js";
7
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
- export const ScrollArea = forwardRef(function ScrollArea({ axis = "vertical", children, onScroll, style, viewportRef, ...properties }, ref) {
10
+ export const ScrollArea = forwardRef(function ScrollArea({ axis = "vertical", children, color, onScroll, style, viewportRef, ...properties }, ref) {
11
11
  const resolved = useResolvedAppearance();
12
12
  const { appearance } = resolved;
13
13
  const direction = resolveDirection(properties.dir, useDirection());
14
- const foreground = resolved.colors.foreground;
14
+ const foreground = resolveColor(color ?? "foreground:base", resolved.colors);
15
15
  const radius = Math.min(appearance.radius, 8);
16
16
  const thickness = Math.max(8, appearance.spacing);
17
17
  const inset = Math.max(2, thickness / 3);
package/dist/select.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useMemo } from "react";
3
- import { Select as AriaSelect, Button, SelectValue, ListBox, ListBoxItem } from "react-aria-components";
3
+ import { Select as AriaSelect, Button, SelectValue } from "react-aria-components";
4
4
  import { controlOpacity, controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
5
5
  import { SurfaceButton } from "./control-surface.js";
6
6
  import { ScrollArea } from "./scroll-area.js";
7
7
  import { PopoverContent } from "./popover.js";
8
8
  import { resolveDirection, useDirection } from "./direction.js";
9
+ import { ListBox } from "./list-box.js";
9
10
  /** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
10
11
  export const Select = forwardRef(function Select({ label, description, errorMessage, disabled, required, invalid, options, value, defaultValue, onChange, size, color, radius, style, material, shadow, ...properties }, ref) {
11
12
  const theme = useControlTheme({ size, color, radius });
@@ -18,19 +19,8 @@ export const Select = forwardRef(function Select({ label, description, errorMess
18
19
  }), 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
20
  width: "var(--trigger-width)",
20
21
  maxHeight: `min(calc(100vh - ${theme.spacing * 2}px), ${theme.height * 8}px)`,
21
- padding: theme.gap,
22
+ padding: 0,
22
23
  display: "flex",
23
24
  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 })] }) });
25
+ }, children: _jsx(ScrollArea, { style: { flex: "1 1 auto", minHeight: 0 }, children: _jsx(ListBox, { color: color, radius: radius, size: size, children: options.map(option => _jsx(ListBox.Item, { id: option.value, textValue: option.label, disabled: option.disabled, children: option.label }, option.value)) }) }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] }) });
36
26
  });
@@ -0,0 +1,18 @@
1
+ import type { Key, Selection } from "react-aria-components";
2
+ export type MultipleStringSelection = readonly string[] | "all";
3
+ export type SingleStringSelectionProps = Readonly<{
4
+ value?: string | null;
5
+ defaultValue?: string | null;
6
+ onChange?: (value: string | null) => void;
7
+ }>;
8
+ export type MultipleStringSelectionProps = Readonly<{
9
+ value?: MultipleStringSelection;
10
+ defaultValue?: MultipleStringSelection;
11
+ onChange?: (value: MultipleStringSelection) => void;
12
+ }>;
13
+ export declare function toAriaSelection(value: string | null | MultipleStringSelection | undefined): "all" | Set<string> | undefined;
14
+ export declare function stringKey(value: Key): string;
15
+ export declare function optionalStringKey(value: Key | null): string | null;
16
+ export declare function singleSelection(selection: Selection): string | null;
17
+ export declare function multipleSelection(selection: Selection): MultipleStringSelection;
18
+ export declare function stringKeys(values: Iterable<Key>): readonly string[];
@@ -0,0 +1,28 @@
1
+ export function toAriaSelection(value) {
2
+ if (value === undefined)
3
+ return undefined;
4
+ if (value === "all")
5
+ return "all";
6
+ if (value === null)
7
+ return new Set();
8
+ if (typeof value === "string")
9
+ return new Set([value]);
10
+ return new Set(value);
11
+ }
12
+ export function stringKey(value) {
13
+ if (typeof value !== "string")
14
+ throw new Error("Collection identities must be strings");
15
+ return value;
16
+ }
17
+ export function optionalStringKey(value) {
18
+ return value === null ? null : stringKey(value);
19
+ }
20
+ export function singleSelection(selection) {
21
+ return selection === "all" ? null : optionalStringKey(selection.values().next().value ?? null);
22
+ }
23
+ export function multipleSelection(selection) {
24
+ return selection === "all" ? "all" : [...selection].map(stringKey);
25
+ }
26
+ export function stringKeys(values) {
27
+ return [...values].map(stringKey);
28
+ }
package/dist/slider.js CHANGED
@@ -46,7 +46,7 @@ export const Slider = forwardRef(function Slider({ label, description, name, for
46
46
  width: vertical ? rail : "100%",
47
47
  height: vertical ? "100%" : rail,
48
48
  borderRadius: rail,
49
- background: theme.paints.neutral.rest.background
49
+ background: theme.paints.subtle.rest.background
50
50
  }, children: [_jsx(BaseSlider.Indicator, { style: {
51
51
  ...theme.transition,
52
52
  borderRadius: rail,
package/dist/spacing.js CHANGED
@@ -3,6 +3,6 @@ export function resolveSpacing(value, appearance) {
3
3
  if (!isScaleLevel(value))
4
4
  return value;
5
5
  if (!appearance)
6
- throw new Error("Semantic spacing requires a UIProvider");
6
+ throw new Error("Semantic spacing requires an Appearance value");
7
7
  return scale(appearance.spacing, value);
8
8
  }
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
@@ -35,15 +35,34 @@ const SurfaceRoot = forwardRef(function Surface({ as: Element = "div", color, ma
35
35
  });
36
36
  export const Surface = SurfaceRoot;
37
37
  function resolveSurface(color, options, resolved) {
38
+ const mode = typeof options === "object" ? "full" : options ?? "basic";
39
+ const level = materialLevel[mode];
38
40
  const material = resolveMaterialOptions(typeof options === "object" ? options : {}, resolved.material);
39
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;
40
47
  return {
41
48
  ...material,
42
- enabled: options !== false,
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,
43
56
  color: fill,
44
57
  foreground: contrastingColor(color ?? defaultColor, resolved.colors)
45
58
  };
46
59
  }
60
+ const materialLevel = Object.freeze({
61
+ none: 0,
62
+ basic: 1,
63
+ extended: 2,
64
+ full: 3
65
+ });
47
66
  const layerStyle = {
48
67
  position: "absolute",
49
68
  inset: 0,
@@ -51,7 +70,7 @@ const layerStyle = {
51
70
  pointerEvents: "none"
52
71
  };
53
72
  function SurfaceLayers({ material, paintTransition }) {
54
- const filtersVisible = material.opacity < 1;
73
+ const filtersVisible = material.fillOpacity < 1;
55
74
  const backdrop = filtersVisible ? material.backdrop : 0;
56
75
  const saturation = filtersVisible ? material.saturation : 1;
57
76
  const distortion = filtersVisible ? material.distortion : 0;
@@ -60,7 +79,7 @@ function SurfaceLayers({ material, paintTransition }) {
60
79
  saturation === 1 ? "" : `saturate(${saturation})`
61
80
  ].filter(Boolean).join(" ");
62
81
  const identity = `phresh-material-${useId().replaceAll(":", "")}`;
63
- 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.opacity, transition: paintTransition })] });
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 })] });
64
83
  }
65
84
  /** Keep refraction and native frost in independent compositor passes. */
66
85
  function BackdropLayer({ filter, name, zIndex }) {