@phreshos/react-ui 0.1.35 → 0.1.37

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 (201) hide show
  1. package/README.md +147 -167
  2. package/dist/accordion.d.ts +7 -6
  3. package/dist/accordion.js +5 -5
  4. package/dist/alert-dialog.js +3 -1
  5. package/dist/alert.d.ts +17 -0
  6. package/dist/alert.js +30 -0
  7. package/dist/app-layout.d.ts +13 -0
  8. package/dist/app-layout.js +84 -0
  9. package/dist/button.d.ts +12 -24
  10. package/dist/button.js +34 -39
  11. package/dist/calendar-layout.d.ts +9 -5
  12. package/dist/calendar-layout.js +67 -78
  13. package/dist/calendar-trigger.d.ts +4 -4
  14. package/dist/calendar-trigger.js +7 -29
  15. package/dist/calendar.d.ts +2 -2
  16. package/dist/calendar.js +6 -5
  17. package/dist/checkbox.d.ts +4 -4
  18. package/dist/checkbox.js +16 -6
  19. package/dist/color-area.d.ts +20 -0
  20. package/dist/color-area.js +30 -0
  21. package/dist/color-field.d.ts +17 -0
  22. package/dist/color-field.js +17 -0
  23. package/dist/color-picker.d.ts +24 -0
  24. package/dist/color-picker.js +30 -0
  25. package/dist/color-slider.d.ts +19 -0
  26. package/dist/color-slider.js +29 -0
  27. package/dist/color-swatch-picker.d.ts +23 -0
  28. package/dist/color-swatch-picker.js +36 -0
  29. package/dist/color-swatch.d.ts +18 -0
  30. package/dist/color-swatch.js +20 -0
  31. package/dist/combo-box.d.ts +18 -10
  32. package/dist/combo-box.js +43 -58
  33. package/dist/context-menu.d.ts +14 -10
  34. package/dist/context-menu.js +7 -6
  35. package/dist/control/color-thumb.d.ts +13 -0
  36. package/dist/control/color-thumb.js +33 -0
  37. package/dist/control/color-value.d.ts +6 -0
  38. package/dist/control/color-value.js +7 -0
  39. package/dist/control/control.d.ts +60 -0
  40. package/dist/control/control.js +71 -0
  41. package/dist/control/field-button.d.ts +13 -0
  42. package/dist/control/field-button.js +28 -0
  43. package/dist/control/field.d.ts +20 -0
  44. package/dist/control/field.js +34 -0
  45. package/dist/control/icon.d.ts +11 -0
  46. package/dist/control/icon.js +8 -0
  47. package/dist/control/item.d.ts +34 -0
  48. package/dist/control/item.js +53 -0
  49. package/dist/control/open-state.d.ts +25 -0
  50. package/dist/control/open-state.js +8 -0
  51. package/dist/control/selection-track.d.ts +39 -0
  52. package/dist/control/selection-track.js +122 -0
  53. package/dist/control/selection.d.ts +44 -0
  54. package/dist/{selection.js → control/selection.js} +16 -0
  55. package/dist/control/surface-render.d.ts +10 -0
  56. package/dist/control/surface-render.js +12 -0
  57. package/dist/control/text-control.d.ts +32 -0
  58. package/dist/control/text-control.js +31 -0
  59. package/dist/control/toggle.d.ts +32 -0
  60. package/dist/control/toggle.js +80 -0
  61. package/dist/date-control.d.ts +9 -9
  62. package/dist/date-control.js +31 -34
  63. package/dist/date-field.d.ts +7 -6
  64. package/dist/date-field.js +8 -6
  65. package/dist/date-picker.d.ts +13 -6
  66. package/dist/date-picker.js +9 -6
  67. package/dist/date-range-picker.d.ts +15 -6
  68. package/dist/date-range-picker.js +12 -9
  69. package/dist/dialog.d.ts +22 -8
  70. package/dist/dialog.js +37 -28
  71. package/dist/disclosure.d.ts +8 -7
  72. package/dist/disclosure.js +31 -52
  73. package/dist/drop-zone.d.ts +18 -0
  74. package/dist/drop-zone.js +32 -0
  75. package/dist/dropdown-menu.d.ts +7 -4
  76. package/dist/dropdown-menu.js +5 -4
  77. package/dist/fieldset.d.ts +13 -0
  78. package/dist/fieldset.js +26 -0
  79. package/dist/file-trigger.d.ts +13 -0
  80. package/dist/file-trigger.js +6 -0
  81. package/dist/flex.d.ts +1 -1
  82. package/dist/flex.js +5 -4
  83. package/dist/foundation/appearance.d.ts +83 -0
  84. package/dist/foundation/appearance.js +38 -0
  85. package/dist/{aria-direction.js → foundation/aria-direction.js} +3 -1
  86. package/dist/foundation/color.d.ts +61 -0
  87. package/dist/foundation/color.js +247 -0
  88. package/dist/{layout.d.ts → foundation/layout.d.ts} +6 -4
  89. package/dist/{layout.js → foundation/layout.js} +7 -5
  90. package/dist/foundation/motion-style.d.ts +9 -0
  91. package/dist/foundation/motion-style.js +50 -0
  92. package/dist/foundation/preferences.d.ts +15 -0
  93. package/dist/{ui-provider.d.ts → foundation/provider.d.ts} +3 -5
  94. package/dist/{ui-provider.js → foundation/provider.js} +14 -26
  95. package/dist/{radius.d.ts → foundation/radius.d.ts} +6 -3
  96. package/dist/foundation/radius.js +11 -0
  97. package/dist/foundation/spacing.d.ts +11 -0
  98. package/dist/foundation/spacing.js +4 -0
  99. package/dist/foundation/visual.d.ts +42 -0
  100. package/dist/{appearance-context.js → foundation/visual.js} +41 -27
  101. package/dist/grid.d.ts +1 -1
  102. package/dist/grid.js +5 -4
  103. package/dist/icons/main.d.ts +5 -3
  104. package/dist/icons/main.js +5 -3
  105. package/dist/input.d.ts +2 -2
  106. package/dist/input.js +9 -7
  107. package/dist/list-box.d.ts +24 -19
  108. package/dist/list-box.js +52 -109
  109. package/dist/main.d.ts +27 -16
  110. package/dist/main.js +22 -9
  111. package/dist/menu.d.ts +28 -32
  112. package/dist/menu.js +47 -93
  113. package/dist/number-field.d.ts +17 -0
  114. package/dist/number-field.js +28 -0
  115. package/dist/panel.d.ts +10 -5
  116. package/dist/panel.js +33 -12
  117. package/dist/popover.d.ts +19 -5
  118. package/dist/popover.js +22 -15
  119. package/dist/progress-bar.d.ts +8 -10
  120. package/dist/progress-bar.js +18 -34
  121. package/dist/radio.d.ts +15 -12
  122. package/dist/radio.js +27 -37
  123. package/dist/range-calendar.d.ts +5 -3
  124. package/dist/range-calendar.js +8 -7
  125. package/dist/scroll-area.d.ts +2 -2
  126. package/dist/scroll-area.js +10 -9
  127. package/dist/segmented-control.d.ts +23 -0
  128. package/dist/segmented-control.js +38 -0
  129. package/dist/select.d.ts +40 -13
  130. package/dist/select.js +63 -18
  131. package/dist/slider.d.ts +7 -3
  132. package/dist/slider.js +51 -50
  133. package/dist/spinner.d.ts +5 -8
  134. package/dist/spinner.js +12 -25
  135. package/dist/surface/grain.d.ts +12 -0
  136. package/dist/surface/grain.js +65 -0
  137. package/dist/{material-options.d.ts → surface/material-options.d.ts} +1 -2
  138. package/dist/{material-options.js → surface/material-options.js} +2 -2
  139. package/dist/surface/paint-class.d.ts +6 -0
  140. package/dist/surface/paint-class.js +36 -0
  141. package/dist/{shadow-options.d.ts → surface/shadow-options.d.ts} +13 -1
  142. package/dist/{shadow-options.js → surface/shadow-options.js} +17 -2
  143. package/dist/surface/surface.d.ts +73 -0
  144. package/dist/surface/surface.js +230 -0
  145. package/dist/switch.d.ts +4 -5
  146. package/dist/switch.js +17 -8
  147. package/dist/table.d.ts +29 -32
  148. package/dist/table.js +68 -106
  149. package/dist/tabs.d.ts +16 -15
  150. package/dist/tabs.js +44 -86
  151. package/dist/textarea.d.ts +2 -2
  152. package/dist/textarea.js +10 -14
  153. package/dist/time-field.d.ts +6 -6
  154. package/dist/time-field.js +8 -6
  155. package/dist/toolbar.d.ts +6 -4
  156. package/dist/toolbar.js +16 -15
  157. package/dist/tooltip.d.ts +13 -4
  158. package/dist/tooltip.js +17 -15
  159. package/dist/tree.d.ts +22 -27
  160. package/dist/tree.js +54 -99
  161. package/dist/window.d.ts +2 -2
  162. package/dist/window.js +18 -20
  163. package/package.json +3 -3
  164. package/dist/appearance-context.d.ts +0 -54
  165. package/dist/appearance.d.ts +0 -11
  166. package/dist/appearance.js +0 -11
  167. package/dist/color.d.ts +0 -69
  168. package/dist/color.js +0 -192
  169. package/dist/control-surface.d.ts +0 -20
  170. package/dist/control-surface.js +0 -11
  171. package/dist/control.d.ts +0 -70
  172. package/dist/control.js +0 -105
  173. package/dist/field-style.d.ts +0 -3
  174. package/dist/field-style.js +0 -12
  175. package/dist/material-paint.d.ts +0 -13
  176. package/dist/material-paint.js +0 -119
  177. package/dist/motion-style.d.ts +0 -13
  178. package/dist/motion-style.js +0 -75
  179. package/dist/preferences.d.ts +0 -12
  180. package/dist/radius.js +0 -11
  181. package/dist/readiness.d.ts +0 -37
  182. package/dist/readiness.js +0 -134
  183. package/dist/selection.d.ts +0 -18
  184. package/dist/spacing.d.ts +0 -8
  185. package/dist/spacing.js +0 -8
  186. package/dist/surface-edge.d.ts +0 -11
  187. package/dist/surface-edge.js +0 -55
  188. package/dist/surface.d.ts +0 -27
  189. package/dist/surface.js +0 -92
  190. package/dist/text-control.d.ts +0 -10
  191. package/dist/text-control.js +0 -0
  192. package/dist/toggle-indicator.d.ts +0 -18
  193. package/dist/toggle-indicator.js +0 -49
  194. /package/dist/{aria-direction.d.ts → foundation/aria-direction.d.ts} +0 -0
  195. /package/dist/{direction.d.ts → foundation/direction.d.ts} +0 -0
  196. /package/dist/{direction.js → foundation/direction.js} +0 -0
  197. /package/dist/{overlay-placement.d.ts → foundation/overlay-placement.d.ts} +0 -0
  198. /package/dist/{overlay-placement.js → foundation/overlay-placement.js} +0 -0
  199. /package/dist/{preferences.js → foundation/preferences.js} +0 -0
  200. /package/dist/{scale.d.ts → foundation/scale.d.ts} +0 -0
  201. /package/dist/{scale.js → foundation/scale.js} +0 -0
@@ -1,23 +1,24 @@
1
1
  import type { CSSProperties, ReactElement, ReactNode } from "react";
2
2
  import type { ButtonProps as AriaButtonProps, DisclosurePanelProps as AriaDisclosurePanelProps, DisclosureProps as AriaDisclosureProps } from "react-aria-components";
3
- import type { ControlColor } from "./control.js";
4
- import type { RadiusProps } from "./radius.js";
5
- import type { ScaleLevel } from "./scale.js";
6
- import { type Spacing } from "./spacing.js";
3
+ import type { Color } from "./foundation/color.js";
4
+ import type { RadiusProps } from "./foundation/radius.js";
5
+ import type { ScaleLevel } from "./foundation/scale.js";
6
+ import { type Spacing } from "./foundation/spacing.js";
7
7
  export interface DisclosureRootProps extends Omit<AriaDisclosureProps, "children" | "className" | "defaultExpanded" | "isDisabled" | "isExpanded" | "onExpandedChange" | "style">, RadiusProps {
8
8
  readonly children?: ReactNode;
9
9
  readonly className?: string;
10
- readonly color?: ControlColor;
10
+ /** Paint of the trigger. Transparent by default, so it only veils on interaction. */
11
+ readonly color?: Color;
11
12
  readonly defaultExpanded?: boolean;
12
13
  readonly disabled?: boolean;
13
14
  readonly expanded?: boolean;
14
- readonly onChange?: (expanded: boolean) => void;
15
+ readonly onExpandedChange?: (expanded: boolean) => void;
15
16
  readonly size?: ScaleLevel;
16
17
  readonly style?: CSSProperties;
17
18
  }
18
19
  /** One independently expandable region. */
19
20
  export declare const DisclosureRoot: import("react").ForwardRefExoticComponent<DisclosureRootProps & import("react").RefAttributes<HTMLDivElement>>;
20
- export interface DisclosureTriggerProps extends Omit<AriaButtonProps, "children" | "className" | "isDisabled" | "onPress" | "style"> {
21
+ export interface DisclosureTriggerProps extends Omit<AriaButtonProps, "children" | "className" | "isDisabled" | "isPending" | "onPress" | "render" | "style"> {
21
22
  readonly children?: ReactNode;
22
23
  readonly className?: string;
23
24
  readonly style?: CSSProperties;
@@ -1,83 +1,62 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, forwardRef, useContext, useMemo } from "react";
3
3
  import { Button as AriaButton, Disclosure as AriaDisclosure, DisclosurePanel as AriaDisclosurePanel, DisclosureStateContext } from "react-aria-components";
4
- import { useResolvedAppearance } from "./appearance-context.js";
5
- import { controlOpacity, useControlTheme } from "./control.js";
6
- import { transitionTiming } from "./motion-style.js";
7
- import { resolveSpacing } from "./spacing.js";
8
- const DisclosureThemeContext = createContext(null);
4
+ import { transition, useControlMetrics } from "./control/control.js";
5
+ import { surfaceRender } from "./control/surface-render.js";
6
+ import { resolveSpacing } from "./foundation/spacing.js";
7
+ import { ChevronDown } from "lucide-react";
8
+ import { iconProps } from "./control/icon.js";
9
+ const DisclosureStyleContext = createContext(null);
9
10
  /** One independently expandable region. */
10
- export const DisclosureRoot = forwardRef(function DisclosureRoot({ children, className, color, defaultExpanded, disabled = false, expanded, onChange, radius = "medium", size = "medium", style, ...properties }, ref) {
11
- const theme = useControlTheme({ color, radius, size });
12
- const resolved = useResolvedAppearance();
13
- const context = useMemo(() => ({
14
- motion: transitionTiming(resolved.transaction, resolved.preferences.animations),
15
- theme
16
- }), [resolved.preferences.animations, resolved.transaction, theme]);
17
- return _jsx(DisclosureThemeContext.Provider, { value: context, children: _jsx(AriaDisclosure, { ...properties, ...(expanded === undefined ? {} : { isExpanded: expanded }), ...(defaultExpanded === undefined ? {} : { defaultExpanded }), ref: ref, className: className, isDisabled: disabled, onExpandedChange: onChange, style: {
18
- display: "grid",
19
- minWidth: 0,
20
- fontFamily: "inherit",
21
- fontSize: theme.fontSize,
22
- ...style
23
- }, children: children }) });
11
+ export const DisclosureRoot = forwardRef(function Disclosure({ children, className, color = "transparent", defaultExpanded, disabled = false, expanded, onExpandedChange, radius, size, style, ...properties }, ref) {
12
+ const metrics = useControlMetrics(size, radius);
13
+ const context = useMemo(() => ({ color, metrics }), [color, metrics]);
14
+ return _jsx(DisclosureStyleContext.Provider, { value: context, children: _jsx(AriaDisclosure, { ...properties, ...(expanded === undefined ? {} : { isExpanded: expanded }), ...(defaultExpanded === undefined ? {} : { defaultExpanded }), ref: ref, className: className, isDisabled: disabled, onExpandedChange: onExpandedChange, style: { display: "grid", minWidth: 0, fontFamily: "inherit", fontSize: metrics.fontSize, ...style }, children: children }) });
24
15
  });
25
16
  /** The control that changes the Disclosure's expansion state. */
26
17
  export const DisclosureTrigger = forwardRef(function DisclosureTrigger({ children, className, style, ...properties }, ref) {
27
- const { motion, theme } = useDisclosureTheme();
18
+ const { color, metrics } = useDisclosureStyle();
28
19
  const disclosure = useDisclosureState();
29
- return _jsxs(AriaButton, { ...properties, ref: ref, slot: "trigger", className: className, style: state => ({
30
- ...theme.transition,
20
+ return _jsxs(AriaButton, { ...properties, ref: ref, slot: "trigger", className: className, render: surfaceRender("button", state => ({
21
+ color,
22
+ radius: metrics.radius,
23
+ shadow: false,
24
+ interaction: { hovered: state.isHovered, pressed: state.isPressed, focusVisible: state.isFocusVisible, disabled: state.isDisabled }
25
+ })), style: {
31
26
  appearance: "none",
32
27
  display: "grid",
33
28
  gridTemplateColumns: "minmax(0, 1fr) auto",
34
29
  alignItems: "center",
35
- gap: theme.gap,
30
+ gap: metrics.gap,
36
31
  width: "100%",
37
32
  minWidth: 0,
38
- minHeight: theme.height,
39
- paddingBlock: theme.gap,
40
- paddingInline: Math.max(8, theme.spacing),
33
+ minHeight: metrics.height,
34
+ paddingBlock: metrics.gap / 2,
35
+ paddingInline: metrics.inset,
41
36
  boxSizing: "border-box",
42
37
  border: 0,
43
- borderRadius: theme.radius,
44
- outline: state.isFocusVisible ? `1px solid ${theme.focusColor}` : "none",
45
- outlineOffset: 1,
46
- cursor: state.isDisabled ? "not-allowed" : "pointer",
47
- opacity: state.isDisabled ? controlOpacity.disabled : 1,
38
+ background: "none",
39
+ cursor: "pointer",
48
40
  font: "inherit",
49
- lineHeight: 1.5,
41
+ fontWeight: 500,
42
+ lineHeight: 1.45,
50
43
  textAlign: "start",
51
44
  userSelect: "none",
52
45
  WebkitTapHighlightColor: "transparent",
53
- ...(disclosure.isExpanded
54
- ? state.isPressed ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest
55
- : state.isPressed ? theme.paints.subtle.pressed : state.isHovered ? theme.paints.subtle.hover : { background: "transparent", color: "inherit" }),
56
46
  ...style
57
- }), children: [_jsx("span", { style: { minWidth: 0 }, children: children }), _jsx("span", { "aria-hidden": "true", style: {
58
- ...motion,
59
- transitionProperty: "transform",
60
- width: "0.5em",
61
- height: "0.5em",
62
- marginInline: "0.25em",
63
- borderInlineEnd: "1px solid currentColor",
64
- borderBlockEnd: "1px solid currentColor",
65
- transform: disclosure.isExpanded ? "rotate(225deg)" : "rotate(45deg)"
66
- } })] });
47
+ }, children: [_jsx("span", { style: { minWidth: 0 }, children: children }), _jsx(ChevronDown, { ...iconProps(14), style: { ...transition(metrics.visual, "rotate"), rotate: disclosure.isExpanded ? "180deg" : "0deg" } })] });
67
48
  });
68
49
  /** Content revealed by a Disclosure trigger. */
69
50
  export const DisclosureContent = forwardRef(function DisclosureContent({ children, className, padding = "small", style, ...properties }, ref) {
70
- const { motion } = useDisclosureTheme();
51
+ const { metrics } = useDisclosureStyle();
71
52
  const disclosure = useDisclosureState();
72
- const { appearance } = useResolvedAppearance();
73
53
  return _jsx(AriaDisclosurePanel, { ...properties, ref: ref, className: className, style: {
74
- ...motion,
75
- transitionProperty: "height, opacity",
54
+ ...transition(metrics.visual, "height, opacity"),
76
55
  height: "var(--disclosure-panel-height)",
77
56
  overflow: "clip",
78
57
  opacity: disclosure.isExpanded ? 1 : 0,
79
58
  ...style
80
- }, children: _jsx("div", { style: { minHeight: 0, overflow: "hidden" }, children: _jsx("div", { style: { padding: resolveSpacing(padding, appearance) }, children: children }) }) });
59
+ }, children: _jsx("div", { style: { minHeight: 0, overflow: "hidden" }, children: _jsx("div", { style: { padding: resolveSpacing(padding, metrics.visual.spacing) }, children: children }) }) });
81
60
  });
82
61
  function useDisclosureState() {
83
62
  const state = useContext(DisclosureStateContext);
@@ -85,8 +64,8 @@ function useDisclosureState() {
85
64
  throw new Error("Disclosure parts must be used inside Disclosure");
86
65
  return state;
87
66
  }
88
- function useDisclosureTheme() {
89
- const context = useContext(DisclosureThemeContext);
67
+ function useDisclosureStyle() {
68
+ const context = useContext(DisclosureStyleContext);
90
69
  if (context == null)
91
70
  throw new Error("Disclosure parts must be used inside Disclosure");
92
71
  return context;
@@ -0,0 +1,18 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { DropZoneProps as AriaDropZoneProps } from "react-aria-components";
3
+ import { type RadiusProps } from "./foundation/radius.js";
4
+ import type { MaterialOverrides } from "./surface/material-options.js";
5
+ export interface DropZoneProps extends Omit<AriaDropZoneProps, "onDrop" | "getDropOperation" | "isDisabled" | "className" | "style" | "children">, RadiusProps, MaterialOverrides {
6
+ readonly children?: ReactNode;
7
+ /** MIME types it accepts, such as `image/png`, or a family such as `image/*`. Anything, when absent. */
8
+ readonly accept?: readonly string[];
9
+ readonly onDrop?: (files: File[]) => void;
10
+ readonly disabled?: boolean;
11
+ readonly className?: string;
12
+ readonly style?: CSSProperties;
13
+ }
14
+ /**
15
+ * An area that takes files dragged onto it. It recesses like every value
16
+ * holder, and takes the subtle `primary` level while files hover it.
17
+ */
18
+ export declare const DropZone: import("react").ForwardRefExoticComponent<DropZoneProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { DropZone as AriaDropZone } from "react-aria-components";
4
+ import { useControlMetrics } from "./control/control.js";
5
+ import { surfaceRender } from "./control/surface-render.js";
6
+ import { colorLevel } from "./foundation/color.js";
7
+ import { resolveRadius } from "./foundation/radius.js";
8
+ import { dimmedClass } from "./surface/surface.js";
9
+ /**
10
+ * An area that takes files dragged onto it. It recesses like every value
11
+ * holder, and takes the subtle `primary` level while files hover it.
12
+ */
13
+ export const DropZone = forwardRef(function DropZone({ children, accept, onDrop, disabled, radius, material, className, style, ...properties }, ref) {
14
+ const metrics = useControlMetrics();
15
+ const { colors } = metrics.visual;
16
+ const accepts = (type) => accept === undefined || accept.some(pattern => pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) : type === pattern);
17
+ return _jsx(AriaDropZone, { ...properties, ref: ref, isDisabled: disabled,
18
+ // A drag names exact types only, so a family such as `image/*` is checked on drop.
19
+ getDropOperation: types => accept === undefined || accept.some(pattern => pattern.endsWith("/*") || types.has(pattern)) ? "copy" : "cancel", onDrop: async (event) => {
20
+ const files = await Promise.all(event.items
21
+ .filter((item) => item.kind === "file" && accepts(item.type))
22
+ .map(item => item.getFile()));
23
+ if (files.length > 0)
24
+ onDrop?.(files);
25
+ }, className: state => dimmedClass(state.isDisabled, className ?? state.defaultClassName) ?? "", render: surfaceRender("div", state => ({
26
+ color: state.isDropTarget ? colorLevel(colors.primary, "subtle", colors) : "background",
27
+ depth: "recessed",
28
+ material,
29
+ radius: resolveRadius(radius ?? "medium", metrics.visual.radius),
30
+ interaction: { hovered: false, focusVisible: state.isFocusVisible || state.isDropTarget, disabled: state.isDisabled }
31
+ })), style: { display: "grid", placeItems: "center", gap: metrics.gap, padding: metrics.spacing * 2, textAlign: "center", ...style }, children: children });
32
+ });
@@ -1,17 +1,20 @@
1
1
  import type { ComponentProps } from "react";
2
2
  import type { MenuTriggerProps as AriaMenuTriggerProps } from "react-aria-components";
3
3
  import { type ButtonProps } from "./button.js";
4
- export type DropdownMenuRootProps = AriaMenuTriggerProps;
5
- export declare function DropdownMenuRoot(properties: DropdownMenuRootProps): import("react").JSX.Element;
4
+ import { type OverlayRootProps } from "./control/open-state.js";
5
+ export type DropdownMenuRootProps = OverlayRootProps & Readonly<Pick<AriaMenuTriggerProps, "trigger">>;
6
+ export declare function DropdownMenuRoot({ children, trigger, ...state }: DropdownMenuRootProps): import("react").JSX.Element;
6
7
  export type DropdownMenuTriggerProps = ButtonProps;
7
8
  export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
8
9
  /** A button-triggered overlay. Its Content owns positioning; Menu owns commands and selection. */
9
10
  export declare const DropdownMenu: typeof DropdownMenuRoot & {
10
11
  Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
11
- Content: import("react").ForwardRefExoticComponent<import("./popover.js").PopoverContentProps & import("react").RefAttributes<HTMLElement>>;
12
+ Content: import("react").ForwardRefExoticComponent<import("./popover.js").PopoverContentProps & Readonly<{
13
+ nonModal?: boolean;
14
+ }> & import("react").RefAttributes<HTMLElement>>;
12
15
  Menu: import("./menu.js").MenuRootComponent & {
13
16
  Item: import("./menu.js").MenuItemComponent;
14
- Section: <T>(props: import("react-aria-components").MenuSectionProps<T> & React.RefAttributes<HTMLElement>) => import("react").ReactElement<unknown, string | React.JSXElementConstructor<any>> | null;
17
+ Section: <T extends object = object>(properties: import("./menu.js").MenuSectionProps<T>) => import("react").ReactElement | null;
15
18
  Header: import("react").ForwardRefExoticComponent<import("react-aria-components").HeaderProps & import("react").RefAttributes<HTMLElement>>;
16
19
  Separator: import("react").ForwardRefExoticComponent<import("react-aria-components").SeparatorProps & import("react").RefAttributes<HTMLElement>>;
17
20
  };
@@ -2,10 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { MenuTrigger as AriaMenuTrigger } from "react-aria-components";
4
4
  import { Button } from "./button.js";
5
+ import { ariaOpenState } from "./control/open-state.js";
5
6
  import { Menu } from "./menu.js";
6
- import { PopoverContent } from "./popover.js";
7
- export function DropdownMenuRoot(properties) {
8
- return _jsx(AriaMenuTrigger, { ...properties });
7
+ import { MenuContent } from "./popover.js";
8
+ export function DropdownMenuRoot({ children, trigger, ...state }) {
9
+ return _jsx(AriaMenuTrigger, { ...ariaOpenState(state), trigger: trigger, children: children });
9
10
  }
10
11
  export const DropdownMenuTrigger = forwardRef(function DropdownMenuTrigger(properties, ref) {
11
12
  return _jsx(Button, { ...properties, ref: ref });
@@ -13,6 +14,6 @@ export const DropdownMenuTrigger = forwardRef(function DropdownMenuTrigger(prope
13
14
  /** A button-triggered overlay. Its Content owns positioning; Menu owns commands and selection. */
14
15
  export const DropdownMenu = Object.assign(DropdownMenuRoot, {
15
16
  Trigger: DropdownMenuTrigger,
16
- Content: PopoverContent,
17
+ Content: MenuContent,
17
18
  Menu
18
19
  });
@@ -0,0 +1,13 @@
1
+ import type { FieldsetHTMLAttributes, ReactNode } from "react";
2
+ export interface FieldsetProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, "title"> {
3
+ /** Names the group for everyone, including assistive technology. */
4
+ readonly title: ReactNode;
5
+ readonly description?: ReactNode;
6
+ readonly children: ReactNode;
7
+ }
8
+ /**
9
+ * A titled group of related fields, such as one section of settings. Each
10
+ * field keeps its own label; the group adds only its title, its description,
11
+ * and the spacing between fields.
12
+ */
13
+ export declare const Fieldset: import("react").ForwardRefExoticComponent<FieldsetProps & import("react").RefAttributes<HTMLFieldSetElement>>;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId } from "react";
3
+ import { controlFontSizes, controlFontWeight, controlOpacity, useControlMetrics } from "./control/control.js";
4
+ import { resolveSpacing } from "./foundation/spacing.js";
5
+ /**
6
+ * A titled group of related fields, such as one section of settings. Each
7
+ * field keeps its own label; the group adds only its title, its description,
8
+ * and the spacing between fields.
9
+ */
10
+ export const Fieldset = forwardRef(function Fieldset({ title, description, children, style, ...properties }, ref) {
11
+ const metrics = useControlMetrics();
12
+ const { spacing } = metrics.visual;
13
+ const titleId = useId();
14
+ const descriptionId = useId();
15
+ // The title names the group through aria-labelledby: a styled legend loses
16
+ // its naming role in some browsers.
17
+ return _jsxs("fieldset", { "aria-labelledby": titleId, "aria-describedby": description != null ? descriptionId : undefined, ...properties, ref: ref, style: {
18
+ display: "grid",
19
+ gap: resolveSpacing("large", spacing),
20
+ minWidth: 0,
21
+ margin: 0,
22
+ padding: 0,
23
+ border: 0,
24
+ ...style
25
+ }, children: [_jsxs("div", { style: { display: "grid", gap: resolveSpacing("xsmall", spacing) }, children: [_jsx("span", { id: titleId, style: { fontSize: controlFontSizes.large, fontWeight: controlFontWeight }, children: title }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: controlFontSizes.medium, opacity: controlOpacity.secondary }, children: description })] }), _jsx("div", { style: { display: "grid", gap: resolveSpacing("large", spacing), minWidth: 0 }, children: children })] });
26
+ });
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ export interface FileTriggerProps {
3
+ /** A Button, or any pressable React UI control, that opens the file chooser. */
4
+ readonly children: ReactNode;
5
+ /** MIME types or extensions the chooser offers, such as `image/*` or `.html`. */
6
+ readonly accept?: readonly string[];
7
+ readonly multiple?: boolean;
8
+ /** Choose a directory instead of files. */
9
+ readonly directory?: boolean;
10
+ readonly onSelect?: (files: File[]) => void;
11
+ }
12
+ /** Opens the system file chooser from the control it wraps. It draws nothing of its own. */
13
+ export declare function FileTrigger({ children, accept, multiple, directory, onSelect }: FileTriggerProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FileTrigger as AriaFileTrigger } from "react-aria-components";
3
+ /** Opens the system file chooser from the control it wraps. It draws nothing of its own. */
4
+ export function FileTrigger({ children, accept, multiple, directory, onSelect }) {
5
+ return _jsx(AriaFileTrigger, { acceptedFileTypes: accept, allowsMultiple: multiple, acceptDirectory: directory, onSelect: files => onSelect?.(files === null ? [] : [...files]), children: children });
6
+ }
package/dist/flex.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ComponentPropsWithoutRef } from "react";
2
- import type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
2
+ import type { LayoutAlignment, LayoutGap, LayoutJustification } from "./foundation/layout.js";
3
3
  /** Properties accepted by the Flex layout primitive. */
4
4
  export interface FlexProps extends ComponentPropsWithoutRef<"div"> {
5
5
  /** Main-axis direction. The browser default is `row`. */
package/dist/flex.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
- import { alignment, justification, resolveGap } from "./layout.js";
4
- import { useAppearance } from "./ui-provider.js";
3
+ import { alignment, justification } from "./foundation/layout.js";
4
+ import { resolveSpacing } from "./foundation/spacing.js";
5
+ import { useVisual } from "./foundation/visual.js";
5
6
  /** A predictable Flexbox container with no visual appearance of its own. */
6
7
  export const Flex = forwardRef(function Flex({ align, direction, gap, inline = false, justify, style, wrap, ...properties }, ref) {
7
- const appearance = useAppearance();
8
+ const { spacing } = useVisual();
8
9
  return _jsx("div", { ...properties, ref: ref, style: {
9
10
  ...style,
10
11
  display: inline ? "inline-flex" : "flex",
11
12
  alignItems: alignment(align) ?? style?.alignItems,
12
13
  flexDirection: direction ?? style?.flexDirection,
13
14
  flexWrap: wrap === undefined ? style?.flexWrap : wrap === "reverse" ? "wrap-reverse" : wrap ? "wrap" : "nowrap",
14
- gap: gap === undefined ? style?.gap : resolveGap(gap, appearance),
15
+ gap: gap === undefined ? style?.gap : resolveSpacing(gap, spacing),
15
16
  justifyContent: justification(justify) ?? style?.justifyContent
16
17
  } });
17
18
  });
@@ -0,0 +1,83 @@
1
+ import { type Appearance as SystemAppearance, type AppearanceColors, type AppearanceMaterial, type AppearanceShadow, type AppearanceTransaction } from "@phreshos/core";
2
+ export type { AppearanceColor, AppearanceColors, AppearanceMaterial, AppearanceRange, AppearanceShadow, AppearanceTransaction, Easing, ThemedValue } from "@phreshos/core";
3
+ /** The System Appearance fields consumed by React UI. */
4
+ export type Appearance = Pick<SystemAppearance, "colors" | "spacing" | "radius" | "shadow" | "material" | "transaction">;
5
+ type Themed<Value> = Readonly<{
6
+ light?: Readonly<Partial<Value>>;
7
+ dark?: Readonly<Partial<Value>>;
8
+ }>;
9
+ type AppearanceUpdateFields = Readonly<{
10
+ colors?: Themed<AppearanceColors>;
11
+ spacing?: number;
12
+ radius?: number;
13
+ shadow?: Themed<AppearanceShadow>;
14
+ material?: Themed<AppearanceMaterial>;
15
+ transaction?: Readonly<Partial<AppearanceTransaction>>;
16
+ }>;
17
+ /** At least one partial visual field merged recursively with the nearest UIProvider. */
18
+ export type AppearanceUpdate = {
19
+ [Field in keyof AppearanceUpdateFields]-?: Readonly<Required<Pick<AppearanceUpdateFields, Field>> & Omit<AppearanceUpdateFields, Field>>;
20
+ }[keyof AppearanceUpdateFields];
21
+ /** Bounds for the visual Appearance fields consumed by React UI. */
22
+ export declare const appearanceLimits: Readonly<{
23
+ spacing: Readonly<{
24
+ minimum: 6;
25
+ maximum: 18;
26
+ }>;
27
+ radius: Readonly<{
28
+ minimum: 6;
29
+ maximum: 18;
30
+ }>;
31
+ shadow: Readonly<{
32
+ x: Readonly<{
33
+ minimum: -48;
34
+ maximum: 48;
35
+ }>;
36
+ y: Readonly<{
37
+ minimum: -48;
38
+ maximum: 48;
39
+ }>;
40
+ blur: Readonly<{
41
+ minimum: 0;
42
+ maximum: 96;
43
+ }>;
44
+ spread: Readonly<{
45
+ minimum: -24;
46
+ maximum: 24;
47
+ }>;
48
+ opacity: Readonly<{
49
+ minimum: 0;
50
+ maximum: 1;
51
+ }>;
52
+ }>;
53
+ material: Readonly<{
54
+ grain: Readonly<{
55
+ minimum: 0;
56
+ maximum: 1;
57
+ }>;
58
+ grainAmount: Readonly<{
59
+ minimum: 0;
60
+ maximum: 1;
61
+ }>;
62
+ backdrop: Readonly<{
63
+ minimum: 0;
64
+ maximum: 24;
65
+ }>;
66
+ opacity: Readonly<{
67
+ minimum: 0;
68
+ maximum: 1;
69
+ }>;
70
+ distortion: Readonly<{
71
+ minimum: 0;
72
+ maximum: 140;
73
+ }>;
74
+ saturation: Readonly<{
75
+ minimum: 1;
76
+ maximum: 2.6;
77
+ }>;
78
+ }>;
79
+ }>;
80
+ /** The visual portion of the System's default Appearance. */
81
+ export declare const defaultAppearance: Appearance;
82
+ /** Applies one partial update; omitted leaves continue to inherit. */
83
+ export declare function mergeAppearance(appearance: Appearance, update: AppearanceUpdate): Appearance;
@@ -0,0 +1,38 @@
1
+ import { appearanceLimits as coreAppearanceLimits, defaultAppearance as systemDefaultAppearance } from "@phreshos/core";
2
+ /** Bounds for the visual Appearance fields consumed by React UI. */
3
+ export const appearanceLimits = Object.freeze({
4
+ spacing: coreAppearanceLimits.spacing,
5
+ radius: coreAppearanceLimits.radius,
6
+ shadow: coreAppearanceLimits.shadow,
7
+ material: coreAppearanceLimits.material
8
+ });
9
+ /** The visual portion of the System's default Appearance. */
10
+ export const defaultAppearance = Object.freeze({
11
+ colors: systemDefaultAppearance.colors,
12
+ spacing: systemDefaultAppearance.spacing,
13
+ radius: systemDefaultAppearance.radius,
14
+ shadow: systemDefaultAppearance.shadow,
15
+ material: systemDefaultAppearance.material,
16
+ transaction: systemDefaultAppearance.transaction
17
+ });
18
+ /** Applies one partial update; omitted leaves continue to inherit. */
19
+ export function mergeAppearance(appearance, update) {
20
+ return Object.freeze({
21
+ colors: mergeThemed(appearance.colors, update.colors),
22
+ spacing: update.spacing ?? appearance.spacing,
23
+ radius: update.radius ?? appearance.radius,
24
+ shadow: mergeThemed(appearance.shadow, update.shadow),
25
+ material: mergeThemed(appearance.material, update.material),
26
+ transaction: update.transaction === undefined
27
+ ? appearance.transaction
28
+ : Object.freeze({ ...appearance.transaction, ...update.transaction })
29
+ });
30
+ }
31
+ function mergeThemed(current, update) {
32
+ if (update === undefined)
33
+ return current;
34
+ return Object.freeze({
35
+ light: update.light === undefined ? current.light : Object.freeze({ ...current.light, ...update.light }),
36
+ dark: update.dark === undefined ? current.dark : Object.freeze({ ...current.dark, ...update.dark })
37
+ });
38
+ }
@@ -3,6 +3,8 @@ import { useMemo } from "react";
3
3
  import { useLocale } from "react-aria/I18nProvider";
4
4
  const ariaDirectionBoundary = "data-phreshos-ui-aria-direction";
5
5
  const redirectedEvents = new WeakSet();
6
+ // Text entry keeps native caret movement; choice inputs move selection by direction.
7
+ const directionalInputTypes = new Set(["radio", "checkbox", "range"]);
6
8
  /** Aligns React Aria's directional keyboard behavior with the React UI direction contract. */
7
9
  export function useAriaDirectionBridge(direction) {
8
10
  const { direction: ariaDirection } = useLocale();
@@ -18,7 +20,7 @@ export function useAriaDirectionBridge(direction) {
18
20
  if (!(target instanceof Element) || target.closest(`[${ariaDirectionBoundary}]`) !== event.currentTarget)
19
21
  return;
20
22
  const editable = target.closest("[contenteditable=true]");
21
- if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || (editable && editable.getAttribute("role") !== "spinbutton"))
23
+ if ((target instanceof HTMLInputElement && !directionalInputTypes.has(target.type)) || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || (editable && editable.getAttribute("role") !== "spinbutton"))
22
24
  return;
23
25
  event.preventDefault();
24
26
  event.stopPropagation();
@@ -0,0 +1,61 @@
1
+ import type { AppearanceColor, AppearanceColors } from "./appearance.js";
2
+ /** A treatment derived from one base color relative to the canvas and content. */
3
+ export type ColorLevel = "subtle" | "soft" | "base" | "strong" | "intense";
4
+ /** Every treatment derived from one base color. */
5
+ export type ColorScale = Readonly<Record<ColorLevel, string>>;
6
+ /** An Appearance color, an Appearance color at one level, or a direct CSS color. */
7
+ export type Color = AppearanceColor | `${AppearanceColor}:${ColorLevel}` | (string & {});
8
+ /** Returns an opaque sRGB serialization, or `null` when the value is not a concrete CSS color. */
9
+ export declare function concreteColor(value: string): string | null;
10
+ /** WCAG relative luminance of a concrete color, or `null` when it cannot be evaluated. */
11
+ export declare function luminance(value: string): number | null;
12
+ /** WCAG contrast ratio between two colors, or `null` when either cannot be evaluated. */
13
+ export declare function contrast(first: string, second: string): number | null;
14
+ /**
15
+ * Mixes `amount` of `target` into `base` in OKLab, concretely when possible.
16
+ * OKLab keeps the hue while moving toward a near-neutral canvas or text color;
17
+ * OKLCH would rotate the hue toward the neutral's meaningless angle.
18
+ */
19
+ export declare function mixColor(base: string, target: string, amount: number): string;
20
+ /** Applies transparency without restricting the source CSS color syntax. */
21
+ export declare function colorOpacity(value: string, opacity: number): string;
22
+ /** Resolves one Color against the active Appearance colors. */
23
+ export declare function resolveColor(value: Color, colors: AppearanceColors): string;
24
+ /** Derives one level from a concrete base color. */
25
+ export declare function colorLevel(base: string, level: ColorLevel, colors: AppearanceColors): string;
26
+ /** Every level derived from one base color. */
27
+ export declare function colorScale(base: string, colors: AppearanceColors): ColorScale;
28
+ /**
29
+ * Perceived lightness contrast (APCA Lc magnitude) of `text` drawn on `fill`,
30
+ * or `null` when either cannot be evaluated. Unlike the WCAG 2 ratio, it is
31
+ * polarity-aware: the WCAG ratio overrates dark text on saturated mid-tone
32
+ * fills such as blue or green, where light text actually reads better.
33
+ */
34
+ export declare function readability(fill: string, text: string): number | null;
35
+ /**
36
+ * Returns the Appearance text color, `foreground`, unless `background` reads
37
+ * clearly better on the fill: better by at least one APCA step.
38
+ */
39
+ export declare function readableColor(fill: string, colors: AppearanceColors): string;
40
+ /** Whether the canvas is darker than its content, derived from the colors themselves. */
41
+ export declare function darkCanvas(colors: AppearanceColors): boolean;
42
+ /**
43
+ * How dark the canvas is, from 0 (a light canvas) to 1 (a dark canvas), read
44
+ * from the perceptual lightness of the background itself. Treatments that
45
+ * depend on the canvas interpolate along it instead of switching at a threshold,
46
+ * so a mid-tone canvas receives a mid-tone treatment.
47
+ */
48
+ export declare function canvasDarkness(colors: AppearanceColors): number;
49
+ /**
50
+ * The paint of a recess in `base`: the same color moved just far enough toward
51
+ * whichever extreme has more room to reach one constant perceptual difference.
52
+ * Depth therefore reads the same on a light, mid-tone, or dark canvas.
53
+ */
54
+ export declare function recessColor(base: string): string;
55
+ /** The lighter Appearance content candidate: the paint of anything lit from above, such as a thumb. */
56
+ export declare function lightColor(colors: AppearanceColors): string;
57
+ export declare function isColorLevel(value: string): value is ColorLevel;
58
+ /** Every level derived from one color in the active Appearance. */
59
+ export declare function useColor(value: Color): ColorScale;
60
+ /** The readable content color for one fill in the active Appearance. */
61
+ export declare function useContrastingColor(value: Color): string;