@phreshos/react-ui 0.1.48 → 0.1.50

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.
@@ -1,15 +1,30 @@
1
1
  import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
2
+ import { type SurfaceDepth } from "./surface/surface.js";
2
3
  export interface AppLayoutProps extends HTMLAttributes<HTMLDivElement> {
3
4
  readonly children: ReactNode;
4
5
  /** The sidebar's width; eighteen times the Appearance spacing by default. */
5
6
  readonly sidebarWidth?: CSSProperties["width"];
6
7
  }
7
8
  export type AppLayoutRegionProps = HTMLAttributes<HTMLElement>;
9
+ export type AppLayoutContentProps = AppLayoutRegionProps & Readonly<{
10
+ /** How the content's Surface stands in the layout: recessed into it by default, or raised. */
11
+ depth?: SurfaceDepth;
12
+ }>;
13
+ export type AppLayoutSidebarProps = AppLayoutRegionProps & Readonly<{
14
+ /** What stays at the foot of the sidebar while the rest scrolls, such as work in progress. */
15
+ footer?: ReactNode;
16
+ }>;
8
17
  export type AppLayoutTitleProps = HTMLAttributes<HTMLHeadingElement>;
9
18
  export declare const AppLayout: import("react").ForwardRefExoticComponent<AppLayoutProps & import("react").RefAttributes<HTMLDivElement>> & {
10
19
  Title: import("react").ForwardRefExoticComponent<AppLayoutTitleProps & import("react").RefAttributes<HTMLHeadingElement>>;
11
- Sidebar: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
20
+ Sidebar: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & Readonly<{
21
+ /** What stays at the foot of the sidebar while the rest scrolls, such as work in progress. */
22
+ footer?: ReactNode;
23
+ }> & import("react").RefAttributes<HTMLElement>>;
12
24
  Header: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
13
- Content: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
25
+ Content: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & Readonly<{
26
+ /** How the content's Surface stands in the layout: recessed into it by default, or raised. */
27
+ depth?: SurfaceDepth;
28
+ }> & import("react").RefAttributes<HTMLElement>>;
14
29
  Footer: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
15
30
  };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, forwardRef, useContext } from "react";
3
3
  import { controlFontSizes, controlFontWeight } from "./control/control.js";
4
4
  import { headerHeight } from "./foundation/layout.js";
@@ -54,10 +54,11 @@ const AppLayoutTitle = forwardRef(function AppLayoutTitle({ style, ...properties
54
54
  ...style
55
55
  } });
56
56
  });
57
- /** The sidebar, such as a program's navigation. It scrolls on its own. */
58
- const AppLayoutSidebar = forwardRef(function AppLayoutSidebar({ children, style, ...properties }, ref) {
57
+ /** The sidebar, such as a program's navigation. It scrolls on its own, above its footer. */
58
+ const AppLayoutSidebar = forwardRef(function AppLayoutSidebar({ children, footer, style, ...properties }, ref) {
59
59
  const { spacing } = useLayout();
60
- return _jsx("aside", { ...properties, ref: ref, style: { gridArea: "sidebar", minHeight: 0, ...style }, children: _jsx(ScrollArea, { style: { height: "100%" }, children: _jsx("div", { style: { padding: scale(spacing, "small") }, children: children }) }) });
60
+ const padding = scale(spacing, "small");
61
+ return _jsxs("aside", { ...properties, ref: ref, style: { gridArea: "sidebar", minHeight: 0, display: "flex", flexDirection: "column", ...style }, children: [_jsx(ScrollArea, { style: { flex: "1 1 auto", minHeight: 0 }, children: _jsx("div", { style: { padding }, children: children }) }), footer != null && _jsx("div", { style: { flex: "none", paddingInline: padding, paddingBottom: padding }, children: footer })] });
61
62
  });
62
63
  /** The header row above the content, as tall as a Window or Panel header. */
63
64
  const AppLayoutHeader = forwardRef(function AppLayoutHeader({ style, ...properties }, ref) {
@@ -73,9 +74,9 @@ const AppLayoutHeader = forwardRef(function AppLayoutHeader({ style, ...properti
73
74
  } });
74
75
  });
75
76
  /** The content, on its Surface. It scrolls on its own, padded by the Appearance spacing. */
76
- const AppLayoutContent = forwardRef(function AppLayoutContent({ children, style, ...properties }, ref) {
77
+ const AppLayoutContent = forwardRef(function AppLayoutContent({ children, depth = "recessed", style, ...properties }, ref) {
77
78
  const { spacing, radius } = useLayout();
78
- return _jsx(Surface, { as: "main", ...properties, ref: ref, material: "extended", radius: resolveRadius("medium", radius), style: {
79
+ return _jsx(Surface, { as: "main", ...properties, ref: ref, depth: depth, material: "extended", radius: resolveRadius("medium", radius), style: {
79
80
  gridArea: "content",
80
81
  minWidth: 0,
81
82
  minHeight: 0,
package/dist/button.d.ts CHANGED
@@ -25,7 +25,10 @@ interface ButtonOwnProps extends ControlProps, RadiusProps, MaterialOverrides, S
25
25
  /** A Button that acts: a native `<button>`. */
26
26
  export interface ButtonActionProps extends NativeButtonProps, ButtonOwnProps {
27
27
  readonly href?: undefined;
28
- /** Prevents activation while keeping the Button focusable. */
28
+ /**
29
+ * Work the Button started is under way: a Spinner shows in its place, the Button keeps its size
30
+ * and its name, and it stays focusable without activating.
31
+ */
29
32
  readonly pending?: boolean;
30
33
  }
31
34
  /**
package/dist/button.js CHANGED
@@ -1,8 +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 { Button as AriaButton, Link as AriaLink } from "react-aria-components";
4
- import { controlOpacity, transition, useControlMetrics } from "./control/control.js";
4
+ import { transition, useControlMetrics } from "./control/control.js";
5
5
  import { surfaceRender } from "./control/surface-render.js";
6
+ import { Spinner } from "./spinner.js";
6
7
  /** A raised Surface you act on, or follow when it has an `href`. */
7
8
  export const Button = forwardRef(function Button(properties, ref) {
8
9
  const { children, color = "default", depth, disabled, iconOnly = false, radius, size, style, className, material, shadow, ...rest } = properties;
@@ -33,7 +34,7 @@ export const Button = forwardRef(function Button(properties, ref) {
33
34
  const { pending = false, type = "button", ...action } = rest;
34
35
  // Disabled state is read from React Aria, so a Button placed in a slot, such
35
36
  // as a calendar's month buttons, follows the owner that disables it.
36
- return _jsx(AriaButton, { ...action, ref: ref, type: type, className: className, isDisabled: disabled, isPending: pending, render: surfaceRender("button", state => surface(state, pending)), style: state => buttonStyle(metrics, iconOnly, !state.isDisabled && !pending && state.isPressed, state.isDisabled, pending, style), children: children });
37
+ return _jsx(AriaButton, { ...action, ref: ref, type: type, className: className, isDisabled: disabled, isPending: pending, render: surfaceRender("button", state => surface(state, pending)), style: state => buttonStyle(metrics, iconOnly, !state.isDisabled && !pending && state.isPressed, state.isDisabled, pending, style), children: pending ? _jsxs(_Fragment, { children: [_jsx("span", { style: { display: "inline-flex", alignItems: "center", gap: metrics.gap, opacity: 0 }, children: children }), _jsx("span", { style: { position: "absolute", inset: 0, display: "grid", placeItems: "center" }, children: _jsx(Spinner, { decorative: true, color: "currentColor", style: { width: metrics.indicator, height: metrics.indicator } }) })] }) : children });
37
38
  });
38
39
  function buttonStyle(metrics, iconOnly, pressed, disabled, pending, style) {
39
40
  return {
@@ -62,7 +63,7 @@ function buttonStyle(metrics, iconOnly, pressed, disabled, pending, style) {
62
63
  WebkitTapHighlightColor: "transparent",
63
64
  cursor: disabled ? "not-allowed" : pending ? "progress" : "pointer",
64
65
  scale: pressed ? "0.97" : "1",
65
- ...(pending ? { opacity: controlOpacity.pending } : {}),
66
+ ...(pending ? { position: "relative" } : {}),
66
67
  ...style
67
68
  };
68
69
  }
@@ -21,6 +21,9 @@ export declare const ContextMenu: typeof ContextMenuRoot & {
21
21
  Section: <T extends object = object>(properties: import("./menu.js").MenuSectionProps<T>) => import("react").ReactElement | null;
22
22
  Header: import("react").ForwardRefExoticComponent<import("react-aria-components").HeaderProps & import("react").RefAttributes<HTMLElement>>;
23
23
  Separator: import("react").ForwardRefExoticComponent<import("react-aria-components").SeparatorProps & import("react").RefAttributes<HTMLElement>>;
24
+ Submenu: (({ children, delay }: import("./menu.js").MenuSubmenuProps) => import("react").JSX.Element) & {
25
+ Content: import("react").ForwardRefExoticComponent<import("./menu.js").MenuSubmenuContentProps & import("react").RefAttributes<HTMLElement>>;
26
+ };
24
27
  };
25
28
  };
26
29
  export type ContextMenuProps = ComponentProps<typeof ContextMenuRoot>;
@@ -36,6 +36,9 @@ export const ContextMenuContent = forwardRef(function ContextMenuContent(propert
36
36
  const target = event.target;
37
37
  if (target instanceof Node && contentRef.current?.contains(target))
38
38
  return;
39
+ // A Submenu opens in its own overlay; a press inside it is still inside this menu.
40
+ if (target instanceof Element && target.closest('[data-trigger="SubmenuTrigger"]'))
41
+ return;
39
42
  state?.close();
40
43
  }
41
44
  document.addEventListener("pointerdown", dismiss, true);
@@ -36,7 +36,6 @@ export declare const controlLineHeight = 1.45;
36
36
  export declare const controlFontWeight = 600;
37
37
  /** Attenuation shared by every control family. */
38
38
  export declare const controlOpacity: Readonly<{
39
- pending: 0.68;
40
39
  secondary: 0.66;
41
40
  placeholder: 0.55;
42
41
  }>;
@@ -21,7 +21,6 @@ export const controlLineHeight = 1.45;
21
21
  export const controlFontWeight = 600;
22
22
  /** Attenuation shared by every control family. */
23
23
  export const controlOpacity = Object.freeze({
24
- pending: 0.68,
25
24
  secondary: 0.66,
26
25
  placeholder: 0.55
27
26
  });
@@ -35,3 +35,8 @@ export declare function itemStyle(metrics: ControlMetrics, disabled: boolean): C
35
35
  export declare function SelectionMark({ visible }: Readonly<{
36
36
  visible: boolean;
37
37
  }>): import("react").JSX.Element;
38
+ /**
39
+ * A collection that a drag would drop into as a whole, outlined as focus is, so it reads as the place
40
+ * that takes the drop.
41
+ */
42
+ export declare function dropTargetOutline(metrics: ControlMetrics, dropTarget: boolean | undefined): CSSProperties;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { colorLevel, mixColor, resolveColor } from "../foundation/color.js";
2
+ import { colorLevel, colorOpacity, mixColor, resolveColor } from "../foundation/color.js";
3
3
  import { clearVeil, interactionShift, surfacePaint } from "../surface/surface.js";
4
4
  import { transition } from "./control.js";
5
5
  import { Check } from "lucide-react";
@@ -55,3 +55,10 @@ export function itemStyle(metrics, disabled) {
55
55
  export function SelectionMark({ visible }) {
56
56
  return _jsx(Check, { ...iconProps(14), style: { flexShrink: 0, marginInlineStart: "auto", opacity: visible ? 1 : 0 } });
57
57
  }
58
+ /**
59
+ * A collection that a drag would drop into as a whole, outlined as focus is, so it reads as the place
60
+ * that takes the drop.
61
+ */
62
+ export function dropTargetOutline(metrics, dropTarget) {
63
+ return dropTarget ? { outline: `3px solid ${colorOpacity(metrics.visual.colors.primary, 0.34)}`, outlineOffset: -3 } : {};
64
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The selection React Aria reported for each value handed out. It also remembers where a range
3
+ * started, which the strings alone lose; when the same value comes back, React Aria gets its own
4
+ * selection again, so Shift extends from the entry chosen last.
5
+ */
6
+ const reported = new WeakMap();
1
7
  export function toAriaSelection(value) {
2
8
  if (value === undefined)
3
9
  return undefined;
@@ -7,7 +13,7 @@ export function toAriaSelection(value) {
7
13
  return new Set();
8
14
  if (typeof value === "string")
9
15
  return new Set([value]);
10
- return new Set(value);
16
+ return reported.get(value) ?? new Set(value);
11
17
  }
12
18
  export function stringKey(value) {
13
19
  if (typeof value !== "string")
@@ -21,7 +27,11 @@ export function singleSelection(selection) {
21
27
  return selection === "all" ? null : optionalStringKey(selection.values().next().value ?? null);
22
28
  }
23
29
  export function multipleSelection(selection) {
24
- return selection === "all" ? "all" : [...selection].map(stringKey);
30
+ if (selection === "all")
31
+ return "all";
32
+ const value = [...selection].map(stringKey);
33
+ reported.set(value, selection);
34
+ return value;
25
35
  }
26
36
  export function stringKeys(values) {
27
37
  return [...values].map(stringKey);
@@ -1,12 +1,17 @@
1
+ import { type ReactElement } from "react";
1
2
  /**
2
3
  * Keeps the document's color scheme on the Theme of the nearest UIProvider,
3
4
  * or on the browser's without one. It writes the Theme into the document's
4
5
  * `<meta name="color-scheme">`, creating the element when the document has
5
6
  * none, before the browser paints, and follows every change. It renders
6
- * nothing; place it once, where the Theme that owns the page is provided.
7
+ * nothing in place; place it once, where the Theme that owns the page is
8
+ * provided.
7
9
  *
8
10
  * A page embedded in a frame needs its color scheme to match the frame's, or
9
11
  * the browser paints the frame opaque instead of letting what is behind it
10
12
  * show.
13
+ *
14
+ * A change of Theme takes the page there in one step; the Desktop around the
15
+ * page carries the movement from one Theme to the other.
11
16
  */
12
- export declare function DocumentTheme(): null;
17
+ export declare function DocumentTheme(): ReactElement;
@@ -1,18 +1,53 @@
1
- import { useLayoutEffect } from "react";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from "react";
2
3
  import { usePreferences } from "./foundation/visual.js";
4
+ /** Marks the document while its Theme changes, so what it paints changes in one step. */
5
+ const changing = "data-phreshos-theme-change";
6
+ /**
7
+ * While the Theme changes, nothing in the document eases toward the new colors: the Desktop that
8
+ * holds the page moves from one Theme to the other as a whole, and a page easing on its own would
9
+ * keep changing after the Desktop has finished. Pseudo-elements carry Surface paint, so they
10
+ * change at once too.
11
+ */
12
+ const instantThemeRule = `
13
+ html[${changing}] *,
14
+ html[${changing}] *::before,
15
+ html[${changing}] *::after {
16
+ transition-delay: 0s !important;
17
+ transition-duration: 0s !important;
18
+ }
19
+ `;
3
20
  /**
4
21
  * Keeps the document's color scheme on the Theme of the nearest UIProvider,
5
22
  * or on the browser's without one. It writes the Theme into the document's
6
23
  * `<meta name="color-scheme">`, creating the element when the document has
7
24
  * none, before the browser paints, and follows every change. It renders
8
- * nothing; place it once, where the Theme that owns the page is provided.
25
+ * nothing in place; place it once, where the Theme that owns the page is
26
+ * provided.
9
27
  *
10
28
  * A page embedded in a frame needs its color scheme to match the frame's, or
11
29
  * the browser paints the frame opaque instead of letting what is behind it
12
30
  * show.
31
+ *
32
+ * A change of Theme takes the page there in one step; the Desktop around the
33
+ * page carries the movement from one Theme to the other.
13
34
  */
14
35
  export function DocumentTheme() {
15
36
  const { theme } = usePreferences();
37
+ const shown = useRef(theme);
38
+ useLayoutEffect(() => {
39
+ if (shown.current === theme)
40
+ return;
41
+ shown.current = theme;
42
+ // Set before the browser paints the new colors, and kept until they have been painted.
43
+ const root = document.documentElement;
44
+ root.setAttribute(changing, "");
45
+ let frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => root.removeAttribute(changing)); });
46
+ return () => {
47
+ cancelAnimationFrame(frame);
48
+ root.removeAttribute(changing);
49
+ };
50
+ }, [theme]);
16
51
  useLayoutEffect(() => {
17
52
  const existing = document.head.querySelector('meta[name="color-scheme" i]');
18
53
  const meta = existing ?? document.head.appendChild(Object.assign(document.createElement("meta"), { name: "color-scheme" }));
@@ -25,5 +60,5 @@ export function DocumentTheme() {
25
60
  meta.remove();
26
61
  };
27
62
  }, [theme]);
28
- return null;
63
+ return _jsx("style", { href: "phreshos-document-theme", precedence: "phreshos", children: instantThemeRule });
29
64
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dragging entries out of a collection and dropping onto it, for Table, GridList, and Tree: pass what
3
+ * `useDragAndDrop` returns as the collection's `dragAndDropHooks`. Drags are the browser's own, so
4
+ * they reach other frames and other applications, and files dragged in from the machine arrive as
5
+ * drop items of kind `file` or `directory`. What a drag would drop into shows as chosen.
6
+ */
7
+ export { useDragAndDrop, type DragAndDropHooks, type DragAndDropOptions, type DropItem, type DropOperation, type DirectoryDropItem, type FileDropItem, type TextDropItem } from "react-aria-components";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dragging entries out of a collection and dropping onto it, for Table, GridList, and Tree: pass what
3
+ * `useDragAndDrop` returns as the collection's `dragAndDropHooks`. Drags are the browser's own, so
4
+ * they reach other frames and other applications, and files dragged in from the machine arrive as
5
+ * drop items of kind `file` or `directory`. What a drag would drop into shows as chosen.
6
+ */
7
+ export { useDragAndDrop } from "react-aria-components";
@@ -0,0 +1,21 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ export interface DrawerProps {
3
+ readonly open: boolean;
4
+ /** Called for a press outside the Drawer or for Escape. */
5
+ readonly onClose: () => void;
6
+ /** Shown at its top, such as the name of what it belongs to. */
7
+ readonly title?: ReactNode;
8
+ readonly "aria-label"?: string;
9
+ readonly className?: string;
10
+ readonly style?: CSSProperties;
11
+ readonly children: ReactNode;
12
+ }
13
+ /**
14
+ * A narrow layout's sidebar, over its content from the start side, such as the navigation a narrow
15
+ * window gives up to its content. It is placed in its nearest positioned container. A press
16
+ * outside or Escape closes it. It slides in and back out along the same path, timed by the
17
+ * Appearance transaction, the way out its own animation so its end is seen; without motion it
18
+ * simply shows and goes. Only its position moves:
19
+ * fading it would switch off the blur it draws of what is behind it.
20
+ */
21
+ export declare function Drawer({ open, onClose, title, className, style, children, ...properties }: DrawerProps): import("react").JSX.Element | null;
package/dist/drawer.js ADDED
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { controlFontSizes, controlFontWeight } from "./control/control.js";
4
+ import MotionStyle, { overlayTransition } from "./foundation/motion-style.js";
5
+ import { scale } from "./foundation/scale.js";
6
+ import { useVisual } from "./foundation/visual.js";
7
+ import { Surface } from "./surface/surface.js";
8
+ /**
9
+ * A narrow layout's sidebar, over its content from the start side, such as the navigation a narrow
10
+ * window gives up to its content. It is placed in its nearest positioned container. A press
11
+ * outside or Escape closes it. It slides in and back out along the same path, timed by the
12
+ * Appearance transaction, the way out its own animation so its end is seen; without motion it
13
+ * simply shows and goes. Only its position moves:
14
+ * fading it would switch off the blur it draws of what is behind it.
15
+ */
16
+ export function Drawer({ open, onClose, title, className, style, children, ...properties }) {
17
+ const visual = useVisual();
18
+ const animated = visual.duration > 0;
19
+ const inset = scale(visual.spacing, "small");
20
+ // It stays while it slides out, and leaves once the slide ends.
21
+ const [present, setPresent] = useState(open);
22
+ useEffect(() => { if (open)
23
+ setPresent(true);
24
+ else if (!animated)
25
+ setPresent(false); }, [open, animated]);
26
+ useEffect(() => {
27
+ if (!open)
28
+ return;
29
+ // Escape closes the Drawer alone, not a surface that holds it.
30
+ const close = (event) => { if (event.key === "Escape") {
31
+ event.preventDefault();
32
+ event.stopPropagation();
33
+ onClose();
34
+ } };
35
+ addEventListener("keydown", close, true);
36
+ return () => removeEventListener("keydown", close, true);
37
+ }, [open, onClose]);
38
+ if (!present)
39
+ return null;
40
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), open && _jsx("div", { "data-drawer-scrim": "", style: { position: "absolute", inset: 0, zIndex: 10 }, onPointerDown: onClose }), _jsxs(Surface, { ...properties, "data-drawer": "", "data-state": animated ? open ? "opening" : "closing" : undefined, material: "full", className: ["phreshos-ui-drawer", className].filter(Boolean).join(" "), onAnimationEnd: () => { if (!open)
41
+ setPresent(false); }, style: {
42
+ ...overlayTransition(visual),
43
+ position: "absolute",
44
+ insetBlock: inset,
45
+ insetInlineStart: inset,
46
+ zIndex: 11,
47
+ boxSizing: "border-box",
48
+ width: `min(${visual.spacing * 20}px, calc(100% - ${visual.spacing * 4}px))`,
49
+ padding: inset,
50
+ overflow: "auto",
51
+ ...style
52
+ }, children: [title != null && _jsx("div", { style: { padding: `${inset}px ${inset}px ${visual.spacing}px`, fontSize: controlFontSizes.xlarge, fontWeight: controlFontWeight }, children: title }), children] })] });
53
+ }
@@ -17,6 +17,9 @@ export declare const DropdownMenu: typeof DropdownMenuRoot & {
17
17
  Section: <T extends object = object>(properties: import("./menu.js").MenuSectionProps<T>) => import("react").ReactElement | null;
18
18
  Header: import("react").ForwardRefExoticComponent<import("react-aria-components").HeaderProps & import("react").RefAttributes<HTMLElement>>;
19
19
  Separator: import("react").ForwardRefExoticComponent<import("react-aria-components").SeparatorProps & import("react").RefAttributes<HTMLElement>>;
20
+ Submenu: (({ children, delay }: import("./menu.js").MenuSubmenuProps) => import("react").JSX.Element) & {
21
+ Content: import("react").ForwardRefExoticComponent<import("./menu.js").MenuSubmenuContentProps & import("react").RefAttributes<HTMLElement>>;
22
+ };
20
23
  };
21
24
  };
22
25
  export type DropdownMenuProps = ComponentProps<typeof DropdownMenuRoot>;
@@ -45,6 +45,22 @@ const stylesheet = `
45
45
  color: inherit;
46
46
  opacity: 0.55;
47
47
  }
48
+ .phreshos-ui-text-control::-webkit-search-cancel-button,
49
+ .phreshos-ui-text-control::-webkit-search-decoration {
50
+ display: none;
51
+ }
52
+ .phreshos-ui-drawer[data-state="opening"] {
53
+ animation: phreshos-ui-drawer-in var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing);
54
+ }
55
+ .phreshos-ui-drawer[data-state="closing"] {
56
+ animation: phreshos-ui-drawer-out var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) forwards;
57
+ }
58
+ :dir(rtl).phreshos-ui-drawer[data-state="opening"] { animation-name: phreshos-ui-drawer-in-rtl; }
59
+ :dir(rtl).phreshos-ui-drawer[data-state="closing"] { animation-name: phreshos-ui-drawer-out-rtl; }
60
+ @keyframes phreshos-ui-drawer-in { from { translate: calc(-100% - 1rem) 0; } }
61
+ @keyframes phreshos-ui-drawer-out { to { translate: calc(-100% - 1rem) 0; } }
62
+ @keyframes phreshos-ui-drawer-in-rtl { from { translate: calc(100% + 1rem) 0; } }
63
+ @keyframes phreshos-ui-drawer-out-rtl { to { translate: calc(100% + 1rem) 0; } }
48
64
  @keyframes phreshos-ui-spin {
49
65
  to { rotate: 360deg; }
50
66
  }
package/dist/grid-list.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { createContext, forwardRef, useContext, useMemo } from "react";
3
3
  import { GridList as AriaGridList, GridListHeader as AriaGridListHeader, GridListItem as AriaGridListItem, GridListSection as AriaGridListSection } from "react-aria-components";
4
4
  import { useControlMetrics } from "./control/control.js";
5
- import { itemSurface, SelectionMark } from "./control/item.js";
5
+ import { dropTargetOutline, itemSurface, SelectionMark } from "./control/item.js";
6
6
  import { ariaSelection } from "./control/selection.js";
7
7
  import { surfaceRender } from "./control/surface-render.js";
8
8
  import { AriaDirectionBoundary } from "./foundation/aria-direction.js";
@@ -17,7 +17,7 @@ const GridListRootImplementation = forwardRef(function GridList(properties, ref)
17
17
  const columns = `repeat(auto-fill, minmax(min(${typeof width === "number" ? `${width}px` : width}, 100%), 1fr))`;
18
18
  const context = useMemo(() => ({ color, metrics, columns }), [color, metrics, columns]);
19
19
  const direction = resolveDirection(native.dir, useDirection());
20
- return _jsx(GridListStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaGridList, { ...native, ...ariaSelection(properties, "single"), ref: ref, dir: direction, layout: "grid", className: className, style: {
20
+ return _jsx(GridListStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaGridList, { ...native, ...ariaSelection(properties, "single"), ref: ref, dir: direction, layout: "grid", className: className, style: state => ({
21
21
  display: "grid",
22
22
  gridTemplateColumns: columns,
23
23
  gap: metrics.gap * 2,
@@ -27,8 +27,10 @@ const GridListRootImplementation = forwardRef(function GridList(properties, ref)
27
27
  outline: "none",
28
28
  fontFamily: "inherit",
29
29
  fontSize: metrics.fontSize,
30
+ borderRadius: metrics.radius,
31
+ ...dropTargetOutline(metrics, state.isDropTarget),
30
32
  ...style
31
- } }) }) });
33
+ }) }) }) });
32
34
  });
33
35
  /** One card. It holds whatever it is given, and shows a check in its corner when selected. */
34
36
  const GridListItemImplementation = forwardRef(function GridListItem({ children, className, color, disabled = false, style, textValue, ...properties }, ref) {
@@ -37,7 +39,8 @@ const GridListItemImplementation = forwardRef(function GridListItem({ children,
37
39
  const { metrics } = inherited;
38
40
  return _jsx(AriaGridListItem, { ...properties, ref: ref, textValue: textValue ?? (typeof children === "string" ? children : undefined), className: className, isDisabled: disabled, render: surfaceRender("div", state => ({
39
41
  ...itemSurface(metrics.visual, itemColor, {
40
- selected: state.isSelected,
42
+ // What a drag would drop into shows as chosen.
43
+ selected: state.isSelected || state.isDropTarget === true,
41
44
  hovered: state.isHovered,
42
45
  pressed: state.isPressed,
43
46
  focusVisible: state.isFocusVisible,
@@ -45,7 +48,7 @@ const GridListItemImplementation = forwardRef(function GridListItem({ children,
45
48
  }, metrics.radius),
46
49
  // Unlike a list row, a card is a whole Surface: flat, in the default color at rest, in its
47
50
  // selection color once selected, with the default material in both.
48
- ...(state.isSelected ? {} : { color: "default" }),
51
+ ...(state.isSelected || state.isDropTarget === true ? {} : { color: "default" }),
49
52
  material: undefined
50
53
  })), style: {
51
54
  position: "relative",
package/dist/loading.d.ts CHANGED
@@ -1,20 +1,23 @@
1
1
  import type { CSSProperties } from "react";
2
2
  import { type ReadinessProps } from "./readiness.js";
3
- import { type SurfaceOwnProps } from "./surface/surface.js";
4
- /** The cover is a Surface: its own props set its color, depth, material, and radius. */
5
- export interface LoadingProps extends Pick<ReadinessProps, "children" | "delay">, SurfaceOwnProps {
3
+ export interface LoadingProps extends Pick<ReadinessProps, "children" | "delay"> {
4
+ /** Class of the layer that shows the loading. */
6
5
  readonly className?: string;
6
+ /** Style of the layer that shows the loading, such as where it stands. */
7
7
  readonly style?: CSSProperties;
8
8
  /** Lists every requirement with a message, each with its own progress, instead of one message. */
9
9
  readonly steps?: boolean;
10
10
  }
11
11
  /**
12
- * A Readiness boundary drawn the way the System draws loading: a Surface that
13
- * covers the waiting interface, with a Spinner and the message of the first
14
- * waiting requirement, or every step. A requirement's detail is its message
15
- * when it is a string; other requirements are waited for without being shown.
12
+ * A Readiness boundary drawn the way the System draws loading: the waiting
13
+ * interface is hidden, not covered, and a Spinner with the message of the
14
+ * first waiting requirement, or every step, stands where it will be. Nothing
15
+ * is painted behind the Spinner, so the Surface the interface sits on shows
16
+ * through. A requirement's detail is its message when it is a string; other
17
+ * requirements are waited for without being shown.
16
18
  *
17
- * The cover fills the nearest positioned ancestor, which owns its shape. It appears at once and
18
- * fades out with the Appearance transaction when everything is ready.
19
+ * The loading fills the nearest positioned ancestor. It appears at once; when
20
+ * everything is ready it fades out with the Appearance transaction and the
21
+ * interface appears whole at once.
19
22
  */
20
- export declare function Loading({ children, delay, steps, ...surface }: LoadingProps): import("react").JSX.Element;
23
+ export declare function Loading({ children, delay, steps, className, style }: LoadingProps): import("react").JSX.Element;
package/dist/loading.js CHANGED
@@ -3,23 +3,34 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { Check } from "lucide-react";
4
4
  import { useControlMetrics } from "./control/control.js";
5
5
  import { iconProps } from "./control/icon.js";
6
- import { Readiness } from "./readiness.js";
6
+ import { Readiness, useReadiness } from "./readiness.js";
7
7
  import { Spinner, spinnerDiameter } from "./spinner.js";
8
- import { Surface } from "./surface/surface.js";
9
8
  import { Text } from "./typography.js";
10
9
  /**
11
- * A Readiness boundary drawn the way the System draws loading: a Surface that
12
- * covers the waiting interface, with a Spinner and the message of the first
13
- * waiting requirement, or every step. A requirement's detail is its message
14
- * when it is a string; other requirements are waited for without being shown.
10
+ * A Readiness boundary drawn the way the System draws loading: the waiting
11
+ * interface is hidden, not covered, and a Spinner with the message of the
12
+ * first waiting requirement, or every step, stands where it will be. Nothing
13
+ * is painted behind the Spinner, so the Surface the interface sits on shows
14
+ * through. A requirement's detail is its message when it is a string; other
15
+ * requirements are waited for without being shown.
15
16
  *
16
- * The cover fills the nearest positioned ancestor, which owns its shape. It appears at once and
17
- * fades out with the Appearance transaction when everything is ready.
17
+ * The loading fills the nearest positioned ancestor. It appears at once; when
18
+ * everything is ready it fades out with the Appearance transaction and the
19
+ * interface appears whole at once.
18
20
  */
19
- export function Loading({ children, delay, steps = false, ...surface }) {
20
- return _jsx(Readiness, { delay: delay, status: state => _jsx(LoadingCover, { state: state, steps: steps, surface: surface }), children: children });
21
+ export function Loading({ children, delay, steps = false, className, style }) {
22
+ return _jsx(Readiness, { delay: delay, status: state => _jsx(LoadingStatus, { state: state, steps: steps, className: className, style: style }), children: _jsx(Hidden, { children: children }) });
21
23
  }
22
- function LoadingCover({ state, steps, surface }) {
24
+ /**
25
+ * Hides the waiting interface while it keeps its layout, so it appears whole and in place. It
26
+ * draws no box of its own, so the interface stays a direct part of the layout around it; such a
27
+ * box cannot fade, which is why the interface appears at once.
28
+ */
29
+ function Hidden({ children }) {
30
+ const { ready } = useReadiness();
31
+ return _jsx("div", { "data-loading-content": "", style: { display: "contents", visibility: ready ? undefined : "hidden" }, children: children });
32
+ }
33
+ function LoadingStatus({ state, steps, className, style }) {
23
34
  const metrics = useControlMetrics();
24
35
  const { duration, easing } = metrics.visual;
25
36
  const { ready } = state;
@@ -39,21 +50,19 @@ function LoadingCover({ state, steps, surface }) {
39
50
  shown.current = state.requirements.flatMap(requirement => typeof requirement.detail === "string" ? [{ message: requirement.detail, ready: requirement.ready }] : []);
40
51
  if (gone)
41
52
  return null;
42
- const { depth = "flat", radius = 0, style, ...paint } = surface;
43
- const cover = {
53
+ const layer = {
44
54
  position: "absolute",
45
55
  inset: 0,
46
56
  display: "grid",
47
57
  placeItems: "center",
48
58
  opacity: ready ? 0 : 1,
49
- // New waiting covers at once; only readiness fades.
59
+ // New waiting shows at once; only readiness fades.
50
60
  transition: ready ? `opacity ${duration}ms ${easing}` : "none",
51
61
  pointerEvents: ready ? "none" : undefined,
52
62
  ...style
53
63
  };
54
64
  const progress = steps ? _jsx(StepList, { steps: shown.current }) : _jsx(Message, { message: shown.current.find(step => !step.ready)?.message });
55
- // Flat and without a shape of its own by default: the container it fills owns its shape.
56
- return _jsx(Surface, { ...paint, depth: depth, radius: radius, role: "status", "aria-hidden": ready || undefined, style: cover, children: progress });
65
+ return _jsx("div", { className: className, role: "status", "aria-hidden": ready || undefined, style: layer, children: progress });
57
66
  }
58
67
  function Message({ message }) {
59
68
  const metrics = useControlMetrics();
package/dist/main.d.ts CHANGED
@@ -17,6 +17,7 @@ export { resolveColor, useColor, useContrastingColor, type Color, type ColorLeve
17
17
  export { Panel, type PanelProps, type PanelHeaderProps, type PanelContentProps } from "./panel.js";
18
18
  export { Window, type WindowProps, type WindowContentProps, type WindowHeaderProps, type WindowIdentityProps, type WindowCenterProps, type WindowActionsProps, type WindowActionProps, type WindowControlProps, type WindowMaximizeProps, type WindowCloseProps, type WindowMoveCaptureProps } from "./window.js";
19
19
  export { default as useWindowMoveHandle, type WindowMoveHandle } from "./use-window-move-handle.js";
20
+ export { Drawer, type DrawerProps } from "./drawer.js";
20
21
  export { Button, type ButtonActionProps, type ButtonColor, type ButtonLinkProps, type ButtonProps } from "./button.js";
21
22
  export { Input, type InputProps } from "./input.js";
22
23
  export { Textarea, type TextareaProps } from "./textarea.js";
@@ -36,7 +37,7 @@ export { ListBox, type ListBoxProps, type ListBoxSingleSelectionProps, type List
36
37
  export { GridList, type GridListProps, type GridListNoSelectionProps, type GridListSingleSelectionProps, type GridListMultipleSelectionProps, type GridListMultipleValue, type GridListItemProps, type GridListSectionProps, type GridListHeaderProps } from "./grid-list.js";
37
38
  export { Tabs, type TabsProps, type TabsListProps, type TabsTabProps, type TabsPanelsProps, type TabsPanelProps } from "./tabs.js";
38
39
  export { Table, type TableProps, type TableNoSelectionProps, type TableSingleSelectionProps, type TableMultipleSelectionProps, type TableMultipleValue, type TableSort, type TableSortDirection, type TableHeaderProps, type TableColumnProps, type TableBodyProps, type TableRowProps, type TableCellProps } from "./table.js";
39
- export { Tree, type TreeProps, type TreeNoSelectionProps, type TreeSingleSelectionProps, type TreeMultipleSelectionProps, type TreeMultipleValue, type TreeItemProps, type TreeContentProps, type TreeCollectionProps } from "./tree.js";
40
+ export { Tree, type TreeProps, type TreeNoSelectionProps, type TreeSingleSelectionProps, type TreeMultipleSelectionProps, type TreeMultipleValue, type TreeItemProps, type TreeContentProps, type TreeCollectionProps, type TreeLoadMoreProps } from "./tree.js";
40
41
  export { Slider, type SliderProps } from "./slider.js";
41
42
  export { ProgressBar, type ProgressBarProps } from "./progress-bar.js";
42
43
  export { Spinner, type SpinnerProps } from "./spinner.js";
@@ -53,13 +54,14 @@ export { Avatar, type AvatarProps } from "./avatar.js";
53
54
  export { TagGroup, type TagGroupProps, type TagGroupTagProps } from "./tag-group.js";
54
55
  export { Skeleton, type SkeletonProps } from "./skeleton.js";
55
56
  export { Snippet, copyText, type SnippetProps } from "./snippet.js";
57
+ export { useDragAndDrop, type DragAndDropHooks, type DragAndDropOptions, type DropItem, type DropOperation, type DirectoryDropItem, type FileDropItem, type TextDropItem } from "./drag-and-drop.js";
56
58
  export { toast, ToastRegion, type ToastContent, type ToastOptions, type ToastRegionProps } from "./toast.js";
57
59
  export { Readiness, useReadiness, useRequirement, type ReadinessProps, type ReadinessState, type Requirement } from "./readiness.js";
58
60
  export { Loading, type LoadingProps } from "./loading.js";
59
61
  export { FileTrigger, type FileTriggerProps } from "./file-trigger.js";
60
62
  export { DropZone, type DropZoneProps } from "./drop-zone.js";
61
63
  export { Fieldset, type FieldsetProps } from "./fieldset.js";
62
- export { AppLayout, type AppLayoutProps, type AppLayoutRegionProps, type AppLayoutTitleProps } from "./app-layout.js";
64
+ export { AppLayout, type AppLayoutContentProps, type AppLayoutProps, type AppLayoutRegionProps, type AppLayoutSidebarProps, type AppLayoutTitleProps } from "./app-layout.js";
63
65
  export { ColorSwatch, type ColorSwatchProps } from "./color-swatch.js";
64
66
  export { ColorSwatchPicker, type ColorSwatchPickerProps, type ColorSwatchPickerItemProps } from "./color-swatch-picker.js";
65
67
  export { ColorSlider, type ColorSliderProps } from "./color-slider.js";
@@ -71,7 +73,7 @@ export { Toolbar, type ToolbarProps, type ToolbarGroupProps, type ToolbarSeparat
71
73
  export { Disclosure, type DisclosureProps, type DisclosureTriggerProps, type DisclosureContentProps } from "./disclosure.js";
72
74
  export { Accordion, type AccordionProps, type AccordionSingleExpansionProps, type AccordionMultipleExpansionProps, type AccordionItemProps } from "./accordion.js";
73
75
  export { Popover, type PopoverProps, type PopoverTriggerProps, type PopoverContentProps, type PopoverDialogProps, type PopoverTitleProps, type PopoverCloseProps } from "./popover.js";
74
- export { Menu, type MenuProps, type MenuNoSelectionProps, type MenuSingleSelectionProps, type MenuMultipleSelectionProps, type MenuMultipleValue, type MenuItemProps, type MenuSectionProps, type MenuHeaderProps, type MenuSeparatorProps } from "./menu.js";
76
+ export { Menu, type MenuProps, type MenuNoSelectionProps, type MenuSingleSelectionProps, type MenuMultipleSelectionProps, type MenuMultipleValue, type MenuItemProps, type MenuSectionProps, type MenuHeaderProps, type MenuSeparatorProps, type MenuSubmenuProps, type MenuSubmenuContentProps } from "./menu.js";
75
77
  export { DropdownMenu, type DropdownMenuProps, type DropdownMenuTriggerProps } from "./dropdown-menu.js";
76
78
  export { ContextMenu, type ContextMenuProps, type ContextMenuTriggerProps, type ContextMenuContentProps } from "./context-menu.js";
77
79
  export { Dialog, type DialogProps, type DialogTriggerProps, type DialogBackdropProps, type DialogBackdropVariant, type DialogContentProps, type DialogHeaderProps, type DialogTitleProps, type DialogDescriptionProps, type DialogBodyProps, type DialogFooterProps, type DialogCloseProps } from "./dialog.js";
package/dist/main.js CHANGED
@@ -17,6 +17,7 @@ export { resolveColor, useColor, useContrastingColor } from "./foundation/color.
17
17
  export { Panel } from "./panel.js";
18
18
  export { Window } from "./window.js";
19
19
  export { default as useWindowMoveHandle } from "./use-window-move-handle.js";
20
+ export { Drawer } from "./drawer.js";
20
21
  export { Button } from "./button.js";
21
22
  export { Input } from "./input.js";
22
23
  export { Textarea } from "./textarea.js";
@@ -52,6 +53,7 @@ export { Avatar } from "./avatar.js";
52
53
  export { TagGroup } from "./tag-group.js";
53
54
  export { Skeleton } from "./skeleton.js";
54
55
  export { Snippet, copyText } from "./snippet.js";
56
+ export { useDragAndDrop } from "./drag-and-drop.js";
55
57
  export { toast, ToastRegion } from "./toast.js";
56
58
  export { Readiness, useReadiness, useRequirement } from "./readiness.js";
57
59
  export { Loading } from "./loading.js";
package/dist/menu.d.ts CHANGED
@@ -4,6 +4,7 @@ import { type MultipleSelectionProps, type MultipleStringSelection, type NoSelec
4
4
  import type { Color } from "./foundation/color.js";
5
5
  import type { RadiusProps } from "./foundation/radius.js";
6
6
  import type { ScaleLevel } from "./foundation/scale.js";
7
+ import { type PopoverContentProps } from "./popover.js";
7
8
  type MenuRootBaseProps<T extends object> = Omit<AriaMenuProps<T>, "className" | "defaultSelectedKeys" | "disabledKeys" | "onAction" | "onSelectionChange" | "selectedKeys" | "selectionMode" | "style" | "shouldFocusWrap" | "shouldCloseOnSelect"> & RadiusProps & Readonly<{
8
9
  className?: string;
9
10
  /** Whether activating an Item closes the menu. Defaults to `true`. */
@@ -38,12 +39,24 @@ export type MenuItemComponent = <T = object>(properties: MenuItemProps<T> & RefA
38
39
  export type MenuSectionProps<T extends object = object> = Omit<AriaMenuSectionProps<T>, "defaultSelectedKeys" | "selectedKeys" | "onSelectionChange" | "selectionMode" | "disallowEmptySelection" | "shouldCloseOnSelect">;
39
40
  export type MenuHeaderProps = AriaHeaderProps;
40
41
  export type MenuSeparatorProps = AriaSeparatorProps;
42
+ export interface MenuSubmenuProps {
43
+ /** The Item that opens the Submenu, then its `Menu.Submenu.Content`. */
44
+ readonly children: [ReactElement, ReactElement];
45
+ /** Milliseconds the pointer rests on the Item before the Submenu opens. */
46
+ readonly delay?: number;
47
+ }
48
+ /** An Item that opens another Menu beside it. */
49
+ declare function MenuSubmenu({ children, delay }: MenuSubmenuProps): import("react").JSX.Element;
50
+ export type MenuSubmenuContentProps = Omit<PopoverContentProps, "placement">;
41
51
  /** A keyboard-navigable collection of commands or selectable options. */
42
52
  export declare const Menu: MenuRootComponent & {
43
53
  Item: MenuItemComponent;
44
54
  Section: <T extends object = object>(properties: MenuSectionProps<T>) => ReactElement | null;
45
55
  Header: import("react").ForwardRefExoticComponent<AriaHeaderProps & RefAttributes<HTMLElement>>;
46
56
  Separator: import("react").ForwardRefExoticComponent<AriaSeparatorProps & RefAttributes<HTMLElement>>;
57
+ Submenu: typeof MenuSubmenu & {
58
+ Content: import("react").ForwardRefExoticComponent<MenuSubmenuContentProps & RefAttributes<HTMLElement>>;
59
+ };
47
60
  };
48
61
  export type MenuProps<T extends object = object> = MenuRootProps<T>;
49
62
  export {};
package/dist/menu.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, forwardRef, useContext, useMemo } from "react";
3
- import { Header as AriaHeader, Menu as AriaMenu, MenuItem as AriaMenuItem, MenuSection as AriaMenuSection, Separator as AriaSeparator } from "react-aria-components";
4
- import { useControlMetrics } from "./control/control.js";
3
+ import { Header as AriaHeader, Menu as AriaMenu, MenuItem as AriaMenuItem, MenuSection as AriaMenuSection, Separator as AriaSeparator, SubmenuTrigger as AriaSubmenuTrigger } from "react-aria-components";
4
+ import { ChevronRight } from "lucide-react";
5
+ import { controlOpacity, useControlMetrics } from "./control/control.js";
5
6
  import { separatorColor } from "./control/field.js";
6
7
  import { itemStyle, itemSurface, SelectionMark } from "./control/item.js";
7
8
  import { ariaSelection, stringKey } from "./control/selection.js";
8
9
  import { surfaceRender } from "./control/surface-render.js";
9
10
  import { collectionHeaderStyle } from "./list-box.js";
11
+ import { resolveDirection, useDirection } from "./foundation/direction.js";
12
+ import { iconProps } from "./control/icon.js";
13
+ import { MenuContent } from "./popover.js";
10
14
  const MenuStyleContext = createContext(null);
11
15
  const MenuRootImplementation = forwardRef(function Menu(properties, ref) {
12
16
  const { className, closeOnSelect, color = "primary", defaultValue: _defaultValue, focusWrap, onAction, onChange: _onChange, radius, selectionMode = "none", size, style, value: _value, ...native } = properties;
@@ -30,6 +34,7 @@ const MenuRootImplementation = forwardRef(function Menu(properties, ref) {
30
34
  const MenuItemImplementation = forwardRef(function MenuItem({ children, className, closeOnSelect, color, disabled = false, style, textValue, ...properties }, ref) {
31
35
  const { color: inherited, metrics, itemRadius, selecting } = useMenuStyle();
32
36
  const itemColor = color ?? inherited;
37
+ const direction = resolveDirection(properties.dir, useDirection());
33
38
  return _jsx(AriaMenuItem, { ...properties, shouldCloseOnSelect: closeOnSelect, ref: ref, textValue: textValue ?? (typeof children === "string" ? children : undefined), className: className, isDisabled: disabled, render: surfaceRender("div", state => itemSurface(metrics.visual, itemColor, {
34
39
  selected: state.isSelected,
35
40
  // Keyboard focus moves the highlight through a menu the way the pointer does;
@@ -38,7 +43,7 @@ const MenuItemImplementation = forwardRef(function MenuItem({ children, classNam
38
43
  pressed: state.isPressed,
39
44
  focusVisible: false,
40
45
  disabled: state.isDisabled
41
- }, itemRadius)), style: { ...itemStyle(metrics, disabled), ...style }, children: state => _jsxs(_Fragment, { children: [typeof children === "function" ? children(state) : children, selecting && _jsx(SelectionMark, { visible: state.isSelected })] }) });
46
+ }, itemRadius)), style: { ...itemStyle(metrics, disabled), ...style }, children: state => _jsxs(_Fragment, { children: [typeof children === "function" ? children(state) : children, selecting && _jsx(SelectionMark, { visible: state.isSelected }), state.hasSubmenu && _jsx(ChevronRight, { ...iconProps(14), style: { marginInlineStart: "auto", opacity: controlOpacity.secondary, rotate: direction === "rtl" ? "180deg" : undefined } })] }) });
42
47
  });
43
48
  const MenuHeader = forwardRef(function MenuHeader({ style, ...properties }, ref) {
44
49
  const { metrics } = useMenuStyle();
@@ -55,6 +60,14 @@ const MenuSeparator = forwardRef(function MenuSeparator({ style, ...properties }
55
60
  ...style
56
61
  } });
57
62
  });
63
+ /** An Item that opens another Menu beside it. */
64
+ function MenuSubmenu({ children, delay }) {
65
+ return _jsx(AriaSubmenuTrigger, { delay: delay, children: children });
66
+ }
67
+ /** The Submenu's own Menu, opening beside its Item toward the reading direction's end. */
68
+ const MenuSubmenuContent = forwardRef(function MenuSubmenuContent(properties, ref) {
69
+ return _jsx(MenuContent, { ...properties, ref: ref, placement: "end top" });
70
+ });
58
71
  function useMenuStyle() {
59
72
  const context = useContext(MenuStyleContext);
60
73
  if (context == null)
@@ -66,5 +79,6 @@ export const Menu = Object.assign(MenuRootImplementation, {
66
79
  Item: MenuItemImplementation,
67
80
  Section: AriaMenuSection,
68
81
  Header: MenuHeader,
69
- Separator: MenuSeparator
82
+ Separator: MenuSeparator,
83
+ Submenu: Object.assign(MenuSubmenu, { Content: MenuSubmenuContent })
70
84
  });
package/dist/spinner.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { ProgressBar as AriaProgressBar } from "react-aria-components";
4
4
  import { useControlMetrics } from "./control/control.js";
5
- import { colorOpacity, resolveColor } from "./foundation/color.js";
6
- import MotionStyle from "./foundation/motion-style.js";
5
+ import { resolveColor } from "./foundation/color.js";
6
+ import MotionStyle, { loopDuration } from "./foundation/motion-style.js";
7
7
  import { scale } from "./foundation/scale.js";
8
8
  /** A standalone status stays visibly distinct from the compact control indicator. */
9
9
  export function spinnerDiameter(spacing, size) {
@@ -25,7 +25,7 @@ export const Spinner = forwardRef(function Spinner({ className, color = "primary
25
25
  verticalAlign: "middle",
26
26
  ...style
27
27
  };
28
- const indicator = _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsxs("svg", { "data-spinner-indicator": "", "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", width: "100%", height: "100%", fill: "none", style: { display: "block", animation: metrics.visual.duration > 0 ? `phreshos-ui-spin ${Math.max(600, metrics.visual.appearance.transaction.duration * 5)}ms linear infinite` : undefined }, children: [_jsx("circle", { "data-spinner-track": "", cx: "12", cy: "12", r: "9", stroke: colorOpacity(arc, 0.2), strokeWidth: "3" }), _jsx("circle", { "data-spinner-fill": "", cx: "12", cy: "12", r: "9", pathLength: "100", stroke: arc, strokeWidth: "3", strokeLinecap: "round", strokeDasharray: "30 70" })] })] });
28
+ const indicator = _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx("svg", { "data-spinner-indicator": "", "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", width: "100%", height: "100%", fill: "none", style: { display: "block", animation: metrics.visual.duration > 0 ? `phreshos-ui-spin ${loopDuration(metrics.visual)}ms linear infinite` : undefined }, children: _jsx("circle", { "data-spinner-fill": "", cx: "12", cy: "12", r: "11", pathLength: "100", stroke: arc, strokeWidth: "2", strokeDasharray: "75 25", transform: "rotate(-90 12 12)" }) })] });
29
29
  if (decorative)
30
30
  return _jsx("div", { ...properties, ref: ref, "aria-hidden": "true", className: className, style: rootStyle, children: indicator });
31
31
  return _jsx(AriaProgressBar, { ...properties, ref: ref, "aria-label": label, className: className, isIndeterminate: true, style: rootStyle, children: indicator });
package/dist/table.js CHANGED
@@ -3,7 +3,7 @@ import { createContext, forwardRef, useContext, useMemo } from "react";
3
3
  import { Cell as AriaCell, Column as AriaColumn, Row as AriaRow, Table as AriaTable, TableBody as AriaTableBody, TableHeader as AriaTableHeader } from "react-aria-components";
4
4
  import { controlFontWeight, controlOpacity, transition, useControlMetrics, secondaryFontSize } from "./control/control.js";
5
5
  import { separatorColor } from "./control/field.js";
6
- import { itemPaint } from "./control/item.js";
6
+ import { dropTargetOutline, itemPaint } from "./control/item.js";
7
7
  import { ariaSelection, stringKey } from "./control/selection.js";
8
8
  import { AriaDirectionBoundary } from "./foundation/aria-direction.js";
9
9
  import { colorOpacity } from "./foundation/color.js";
@@ -18,7 +18,7 @@ const TableRoot = forwardRef(function Table(properties, ref) {
18
18
  const metrics = useControlMetrics(size, radius);
19
19
  const context = useMemo(() => ({ color, interactive: selectionMode !== "none" || onAction != null, metrics }), [color, metrics, onAction, selectionMode]);
20
20
  const direction = resolveDirection(native.dir, useDirection());
21
- return _jsx(TableStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaTable, { ...native, shouldSelectOnPressUp: selectOnPressUp, ...ariaSelection(properties, "none"), ...(sort === undefined ? {} : { sortDescriptor: sort }), ref: ref, dir: direction, className: className, onRowAction: onAction == null ? undefined : key => onAction(stringKey(key)), onSortChange: onSortChange == null ? undefined : descriptor => onSortChange(tableSort(descriptor)), style: {
21
+ return _jsx(TableStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaTable, { ...native, shouldSelectOnPressUp: selectOnPressUp, ...ariaSelection(properties, "none"), ...(sort === undefined ? {} : { sortDescriptor: sort }), ref: ref, dir: direction, className: className, onRowAction: onAction == null ? undefined : key => onAction(stringKey(key)), onSortChange: onSortChange == null ? undefined : descriptor => onSortChange(tableSort(descriptor)), style: state => ({
22
22
  width: "100%",
23
23
  minWidth: "max-content",
24
24
  borderCollapse: "separate",
@@ -29,8 +29,9 @@ const TableRoot = forwardRef(function Table(properties, ref) {
29
29
  overflow: "hidden",
30
30
  fontFamily: "inherit",
31
31
  fontSize: metrics.fontSize,
32
+ ...dropTargetOutline(metrics, state.isDropTarget),
32
33
  ...style
33
- } }) }) });
34
+ }) }) }) });
34
35
  });
35
36
  function tableSort(descriptor) {
36
37
  return { column: stringKey(descriptor.column), direction: descriptor.direction };
@@ -68,7 +69,8 @@ const TableRowImplementation = forwardRef(function TableRow({ className, disable
68
69
  return _jsx(AriaRow, { ...properties, ref: ref, className: state => dimmedClass(state.isDisabled, className ?? state.defaultClassName) ?? "", isDisabled: disabled, onAction: onAction, style: state => ({
69
70
  ...transition(metrics.visual, "background-color, color, outline-color"),
70
71
  ...itemPaint(metrics.visual, color, {
71
- selected: state.isSelected,
72
+ // What a drag would drop into shows as chosen.
73
+ selected: state.isSelected || state.isDropTarget === true,
72
74
  hovered: responds && state.isHovered,
73
75
  pressed: responds && state.isPressed,
74
76
  focusVisible: false,
package/dist/tooltip.d.ts CHANGED
@@ -4,7 +4,9 @@ import { type ButtonActionProps } from "./button.js";
4
4
  import { type AriaOverlayInternals, type OverlayRootProps } from "./control/open-state.js";
5
5
  import { type Direction } from "./foundation/direction.js";
6
6
  import { type SurfaceOwnProps } from "./surface/surface.js";
7
- export type TooltipRootProps = OverlayRootProps & Readonly<Pick<AriaTooltipTriggerProps, "delay" | "closeDelay" | "trigger">> & Readonly<{
7
+ export type TooltipRootProps = OverlayRootProps & Readonly<Pick<AriaTooltipTriggerProps, "closeDelay" | "trigger">> & Readonly<{
8
+ /** Milliseconds the pointer rests on the trigger before the Tooltip opens: four Appearance transactions by default. */
9
+ delay?: number;
8
10
  /** Whether the Tooltip is prevented from opening. */
9
11
  disabled?: boolean;
10
12
  /** Whether pressing the trigger closes the Tooltip. Defaults to `true`. */
@@ -18,6 +20,8 @@ export interface TooltipContentProps extends Omit<AriaTooltipProps, AriaOverlayI
18
20
  readonly children?: ReactNode;
19
21
  readonly className?: string;
20
22
  readonly dir?: Direction;
23
+ /** DOM container that owns the positioned overlay's coordinate space. */
24
+ readonly portalContainer?: Element;
21
25
  readonly style?: CSSProperties;
22
26
  }
23
27
  export declare const TooltipContent: import("react").ForwardRefExoticComponent<TooltipContentProps & import("react").RefAttributes<HTMLDivElement>>;
package/dist/tooltip.js CHANGED
@@ -12,17 +12,19 @@ import { useVisual } from "./foundation/visual.js";
12
12
  import { floatingShadow } from "./surface/shadow-options.js";
13
13
  import { Surface } from "./surface/surface.js";
14
14
  export function TooltipRoot({ children, delay, closeDelay, trigger, disabled, closeOnPress, ...state }) {
15
- return _jsx(AriaTooltipTrigger, { ...ariaOpenState(state), delay: delay, closeDelay: closeDelay, trigger: trigger, isDisabled: disabled, shouldCloseOnPress: closeOnPress, children: children });
15
+ // A pointer resting on a trigger for four Appearance transactions is asking what it is.
16
+ const rest = useVisual().appearance.transaction.duration * 4;
17
+ return _jsx(AriaTooltipTrigger, { ...ariaOpenState(state), delay: delay ?? rest, closeDelay: closeDelay, trigger: trigger, isDisabled: disabled, shouldCloseOnPress: closeOnPress, children: children });
16
18
  }
17
19
  export const TooltipTrigger = forwardRef(function TooltipTrigger(properties, ref) {
18
20
  return _jsx(Button, { ...properties, ref: ref });
19
21
  });
20
- export const TooltipContent = forwardRef(function TooltipContent({ children, className, color, material, radius, shadow, style, offset, placement = "top", dir, ...attributes }, ref) {
22
+ export const TooltipContent = forwardRef(function TooltipContent({ children, className, color, material, radius, shadow, style, offset, placement = "top", dir, portalContainer, ...attributes }, ref) {
21
23
  const visual = useVisual();
22
24
  const inset = scale(visual.spacing, "small");
23
25
  const transition = overlayTransition(visual);
24
26
  const direction = resolveDirection(dir, useDirection());
25
- return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaTooltip, { ...attributes, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color ?? "foreground", material: material, radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
27
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaTooltip, { ...attributes, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), UNSTABLE_portalContainer: portalContainer, className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color ?? "foreground", material: material, radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
26
28
  boxSizing: "border-box",
27
29
  maxWidth: "28em",
28
30
  paddingBlock: inset,
package/dist/tree.d.ts CHANGED
@@ -41,11 +41,21 @@ export interface TreeContentProps extends AriaTreeItemContentProps {
41
41
  readonly children: AriaTreeItemContentProps["children"];
42
42
  }
43
43
  export type TreeCollectionProps<T extends object = object> = AriaCollectionProps<T>;
44
+ export type TreeLoadMoreProps = Readonly<{
45
+ /** Asks for more Items once the end of the list comes near while scrolling. */
46
+ onLoadMore: () => void;
47
+ }>;
48
+ /**
49
+ * The end of a list shown in parts, such as a long folder: when scrolling brings it near, the
50
+ * list is asked for more. It is placed after the Items it follows, at their level, and draws nothing.
51
+ */
52
+ declare function TreeLoadMore({ onLoadMore }: TreeLoadMoreProps): import("react").JSX.Element;
44
53
  /** Hierarchical navigation with independently controlled expansion, selection, and actions. */
45
54
  export declare const Tree: TreeRootComponent & {
46
55
  Item: TreeItemComponent;
47
56
  Content: import("react").ForwardRefExoticComponent<TreeContentProps & RefAttributes<Element>>;
48
57
  Collection: typeof AriaCollection;
58
+ LoadMore: typeof TreeLoadMore;
49
59
  };
50
60
  export type TreeProps<T extends object = object> = TreeRootProps<T>;
51
61
  export {};
package/dist/tree.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, forwardRef, useContext, useMemo } from "react";
3
- import { Button as AriaButton, Collection as AriaCollection, Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent as AriaTreeItemContent } from "react-aria-components";
3
+ import { Button as AriaButton, Collection as AriaCollection, Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent as AriaTreeItemContent, TreeLoadMoreItem as AriaTreeLoadMoreItem } from "react-aria-components";
4
4
  import { proportionalRadius, transition, useControlMetrics } from "./control/control.js";
5
- import { itemPaint, itemStyle } from "./control/item.js";
5
+ import { dropTargetOutline, itemPaint, itemStyle } from "./control/item.js";
6
6
  import { ariaSelection, stringKey, stringKeys } from "./control/selection.js";
7
7
  import { AriaDirectionBoundary } from "./foundation/aria-direction.js";
8
8
  import { colorOpacity } from "./foundation/color.js";
@@ -18,7 +18,7 @@ const TreeRootImplementation = forwardRef(function Tree(properties, ref) {
18
18
  const itemRadius = metrics.radius;
19
19
  const direction = resolveDirection(native.dir, useDirection());
20
20
  const context = useMemo(() => ({ color, direction, interactive: selectionMode !== "none" || onAction != null, metrics, itemRadius }), [color, direction, metrics, itemRadius, onAction, selectionMode]);
21
- return _jsx(TreeStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaTree, { ...native, shouldSelectOnPressUp: selectOnPressUp, ...ariaSelection(properties, "none"), ...(expanded !== undefined ? { expandedKeys: new Set(expanded) } : {}), ...(defaultExpanded !== undefined ? { defaultExpandedKeys: new Set(defaultExpanded) } : {}), ...(onExpandedChange !== undefined ? { onExpandedChange: (keys) => onExpandedChange(stringKeys(keys)) } : {}), ref: ref, dir: direction, className: className, onAction: onAction == null ? undefined : key => onAction(stringKey(key)), style: {
21
+ return _jsx(TreeStyleContext.Provider, { value: context, children: _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaTree, { ...native, shouldSelectOnPressUp: selectOnPressUp, ...ariaSelection(properties, "none"), ...(expanded !== undefined ? { expandedKeys: new Set(expanded) } : {}), ...(defaultExpanded !== undefined ? { defaultExpandedKeys: new Set(defaultExpanded) } : {}), ...(onExpandedChange !== undefined ? { onExpandedChange: (keys) => onExpandedChange(stringKeys(keys)) } : {}), ref: ref, dir: direction, className: className, onAction: onAction == null ? undefined : key => onAction(stringKey(key)), style: state => ({
22
22
  display: "grid",
23
23
  alignContent: "start",
24
24
  gap: 2,
@@ -28,8 +28,10 @@ const TreeRootImplementation = forwardRef(function Tree(properties, ref) {
28
28
  outline: "none",
29
29
  fontFamily: "inherit",
30
30
  fontSize: metrics.fontSize,
31
+ borderRadius: metrics.radius,
32
+ ...dropTargetOutline(metrics, state.isDropTarget),
31
33
  ...style
32
- } }) }) });
34
+ }) }) }) });
33
35
  });
34
36
  /** Tree entries use the collection Item treatment as their own paint. */
35
37
  const TreeItemImplementation = forwardRef(function TreeItem({ arrowNavigation, className, disabled = false, expandable, onAction, style, ...properties }, ref) {
@@ -39,7 +41,8 @@ const TreeItemImplementation = forwardRef(function TreeItem({ arrowNavigation, c
39
41
  return {
40
42
  ...itemStyle(metrics, state.isDisabled),
41
43
  ...itemPaint(metrics.visual, color, {
42
- selected: state.isSelected,
44
+ // What a drag would drop into shows as chosen.
45
+ selected: state.isSelected || state.isDropTarget === true,
43
46
  hovered: responds && state.isHovered,
44
47
  pressed: responds && state.isPressed,
45
48
  focusVisible: false,
@@ -75,6 +78,13 @@ const TreeContent = forwardRef(function TreeContent({ children }, ref) {
75
78
  }), children: _jsx(ChevronRight, { ...iconProps(14), style: { ...transition(metrics.visual, "rotate"), rotate: state.isExpanded ? "90deg" : direction === "rtl" ? "180deg" : "0deg" } }) })
76
79
  : _jsx("span", { "aria-hidden": "true", style: { flex: `0 0 ${box}px` } }), _jsx("span", { style: { flex: "1 1 auto", minWidth: 0, display: "flex", alignItems: "center", gap: metrics.gap }, children: typeof children === "function" ? children(state) : children })] }) });
77
80
  });
81
+ /**
82
+ * The end of a list shown in parts, such as a long folder: when scrolling brings it near, the
83
+ * list is asked for more. It is placed after the Items it follows, at their level, and draws nothing.
84
+ */
85
+ function TreeLoadMore({ onLoadMore }) {
86
+ return _jsx(AriaTreeLoadMoreItem, { onLoadMore: onLoadMore });
87
+ }
78
88
  function useTreeStyle() {
79
89
  const context = useContext(TreeStyleContext);
80
90
  if (context == null)
@@ -85,5 +95,6 @@ function useTreeStyle() {
85
95
  export const Tree = Object.assign(TreeRootImplementation, {
86
96
  Item: TreeItemImplementation,
87
97
  Content: TreeContent,
88
- Collection: AriaCollection
98
+ Collection: AriaCollection,
99
+ LoadMore: TreeLoadMore
89
100
  });
@@ -1,4 +1,4 @@
1
- import type { BeginWindowMoveGesture } from "@phreshos/core";
1
+ import type { BeginPresentationMoveGesture } from "@phreshos/core";
2
2
  import { type PointerEventHandler } from "react";
3
3
  export interface WindowMoveHandle {
4
4
  onPointerDown: PointerEventHandler<HTMLElement>;
@@ -8,4 +8,4 @@ export interface WindowMoveHandle {
8
8
  onLostPointerCapture: PointerEventHandler<HTMLElement>;
9
9
  }
10
10
  /** Turns one DOM element into a move handle for any compatible host gesture. */
11
- export default function useWindowMoveHandle(beginMoveGesture?: BeginWindowMoveGesture, onError?: (error: unknown) => void): WindowMoveHandle;
11
+ export default function useWindowMoveHandle(beginMoveGesture?: BeginPresentationMoveGesture, onError?: (error: unknown) => void): WindowMoveHandle;
package/dist/window.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
- import type { BeginWindowMoveGesture } from "@phreshos/core";
2
+ import type { BeginPresentationMoveGesture } from "@phreshos/core";
3
3
  import { type ButtonActionProps } from "./button.js";
4
4
  import { type Color } from "./foundation/color.js";
5
5
  import { type SurfaceProps } from "./surface/surface.js";
@@ -16,7 +16,7 @@ export interface WindowHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>,
16
16
  /** Base color inherited by header-owned text and icons. */
17
17
  readonly color?: Color;
18
18
  /** Hands an intentional header drag to its host after the pointer threshold. */
19
- readonly beginMoveGesture?: BeginWindowMoveGesture;
19
+ readonly beginMoveGesture?: BeginPresentationMoveGesture;
20
20
  /** Whether the represented window is currently maximized. */
21
21
  readonly maximized?: boolean;
22
22
  /** Toggles the represented window between maximized and restored states. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -57,7 +57,7 @@
57
57
  "dependencies": {
58
58
  "@base-ui/react": "^1.7.0",
59
59
  "@internationalized/date": "^3.12.3",
60
- "@phreshos/core": "^0.1.65",
60
+ "@phreshos/core": "^0.1.67",
61
61
  "colorjs.io": "^0.7.1",
62
62
  "lucide-react": "^1.48.0",
63
63
  "react-aria": "^3.51.0",