@phreshos/react-ui 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.d.ts CHANGED
@@ -14,6 +14,7 @@ export { Surface, type MaterialMode, type MaterialOptions, type ShadowOptions, t
14
14
  export { defaultColor, type Color, type ColorLevel, type ColorScale } from "./color.js";
15
15
  export { Panel, type PanelProps, type PanelHeaderProps, type PanelContentProps } from "./panel.js";
16
16
  export { Window, type WindowProps, type WindowContentProps, type WindowHeaderProps, type WindowIdentityProps, type WindowCenterProps, type WindowActionsProps, type WindowActionProps, type WindowControlProps, type WindowMaximizeProps, type WindowCloseProps } from "./window.js";
17
+ export { default as useWindowMoveHandle, type WindowMoveHandle } from "./use-window-move-handle.js";
17
18
  export { Button, type ButtonColor, type ButtonProps } from "./button.js";
18
19
  export { Input, type InputProps } from "./input.js";
19
20
  export { Textarea, type TextareaProps } from "./textarea.js";
package/dist/main.js CHANGED
@@ -13,6 +13,7 @@ export { defaultColor } from "./color.js";
13
13
  // Context-dependent parts are public only through their owning component family.
14
14
  export { Panel } from "./panel.js";
15
15
  export { Window } from "./window.js";
16
+ export { default as useWindowMoveHandle } from "./use-window-move-handle.js";
16
17
  export { Button } from "./button.js";
17
18
  export { Input } from "./input.js";
18
19
  export { Textarea } from "./textarea.js";
@@ -0,0 +1,11 @@
1
+ import type { BeginWindowMoveGesture } from "@phreshos/core";
2
+ import { type PointerEventHandler } from "react";
3
+ export interface WindowMoveHandle {
4
+ onPointerDown: PointerEventHandler<HTMLElement>;
5
+ onPointerMove: PointerEventHandler<HTMLElement>;
6
+ onPointerUp: PointerEventHandler<HTMLElement>;
7
+ onPointerCancel: PointerEventHandler<HTMLElement>;
8
+ onLostPointerCapture: PointerEventHandler<HTMLElement>;
9
+ }
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;
@@ -0,0 +1,114 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ /** Turns one DOM element into a move handle for any compatible host gesture. */
3
+ export default function useWindowMoveHandle(beginMoveGesture, onError) {
4
+ const active = useRef(null);
5
+ const beginHandler = useRef(beginMoveGesture);
6
+ const errorHandler = useRef(onError);
7
+ beginHandler.current = beginMoveGesture;
8
+ errorHandler.current = onError;
9
+ useEffect(() => () => {
10
+ active.current?.gesture?.cancel();
11
+ active.current = null;
12
+ }, []);
13
+ return useMemo(() => {
14
+ const point = (event) => ({ x: event.clientX, y: event.clientY });
15
+ const matching = (event) => active.current?.pointer === event.pointerId;
16
+ const releaseCapture = (move) => {
17
+ if (move.element.hasPointerCapture(move.pointer))
18
+ move.element.releasePointerCapture(move.pointer);
19
+ };
20
+ const fail = (error) => {
21
+ if (errorHandler.current)
22
+ errorHandler.current(error);
23
+ else if (typeof globalThis.reportError === "function")
24
+ globalThis.reportError(error);
25
+ else
26
+ setTimeout(() => { throw error; });
27
+ };
28
+ const cancel = (event) => {
29
+ if (!matching(event))
30
+ return;
31
+ const move = active.current;
32
+ active.current = null;
33
+ move?.gesture?.cancel();
34
+ if (move)
35
+ releaseCapture(move);
36
+ };
37
+ return {
38
+ onPointerDown(event) {
39
+ if (!beginHandler.current || !event.isPrimary || event.button !== 0 || active.current)
40
+ return;
41
+ active.current = {
42
+ pointer: event.pointerId,
43
+ element: event.currentTarget,
44
+ origin: point(event),
45
+ gesture: null,
46
+ handedOff: false
47
+ };
48
+ event.currentTarget.setPointerCapture(event.pointerId);
49
+ },
50
+ onPointerMove(event) {
51
+ const move = active.current;
52
+ const begin = beginHandler.current;
53
+ if (!begin || !matching(event) || !move || move.gesture)
54
+ return;
55
+ const current = point(event);
56
+ if (Math.hypot(current.x - move.origin.x, current.y - move.origin.y) < 4)
57
+ return;
58
+ try {
59
+ // Capture proves that an intentional drag began. Releasing it lets
60
+ // the ready host receive the continuous pointer stream without transport.
61
+ const gesture = begin({ origin: move.origin, point: current });
62
+ move.gesture = gesture;
63
+ // A failed handoff is reported through `ready`; its corresponding
64
+ // completion may reject as the same failure and must not escape alone.
65
+ void gesture.finished.catch(() => undefined);
66
+ void gesture.ready.then(() => {
67
+ if (active.current !== move) {
68
+ gesture.cancel();
69
+ return gesture.finished;
70
+ }
71
+ move.handedOff = true;
72
+ releaseCapture(move);
73
+ return gesture.finished;
74
+ }).then(() => {
75
+ if (active.current === move)
76
+ active.current = null;
77
+ }, error => {
78
+ if (active.current === move) {
79
+ active.current = null;
80
+ releaseCapture(move);
81
+ }
82
+ fail(error);
83
+ });
84
+ }
85
+ catch (error) {
86
+ active.current = null;
87
+ releaseCapture(move);
88
+ fail(error);
89
+ }
90
+ },
91
+ onPointerUp(event) {
92
+ if (!matching(event))
93
+ return;
94
+ const move = active.current;
95
+ active.current = null;
96
+ move?.gesture?.cancel();
97
+ if (move)
98
+ releaseCapture(move);
99
+ },
100
+ onPointerCancel: cancel,
101
+ onLostPointerCapture(event) {
102
+ if (!matching(event))
103
+ return;
104
+ // Losing capture after handoff is expected because the host now owns
105
+ // the pointer. Before handoff it means the local candidate was abandoned.
106
+ const move = active.current;
107
+ if (move && !move.handedOff) {
108
+ active.current = null;
109
+ move.gesture?.cancel();
110
+ }
111
+ }
112
+ };
113
+ }, []);
114
+ }
package/dist/window.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
+ import type { BeginWindowMoveGesture } from "@phreshos/core";
2
3
  import { type ButtonProps } from "./button.js";
3
4
  import { type Color } from "./color.js";
4
5
  import { type SurfaceProps } from "./surface.js";
@@ -11,6 +12,10 @@ export interface WindowHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>,
11
12
  readonly active?: boolean;
12
13
  /** Base color inherited by header-owned text and icons. */
13
14
  readonly color?: Color;
15
+ /** Hands an intentional header drag to its host after the pointer threshold. */
16
+ readonly beginMoveGesture?: BeginWindowMoveGesture;
17
+ /** Receives a synchronous or asynchronous move handoff failure. */
18
+ readonly onMoveError?: (error: unknown) => void;
14
19
  }
15
20
  export interface WindowIdentityProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
16
21
  readonly icon?: string;
package/dist/window.js CHANGED
@@ -6,6 +6,7 @@ import { resolveColor } from "./color.js";
6
6
  import { transitionTiming } from "./motion-style.js";
7
7
  import { scale } from "./scale.js";
8
8
  import { Surface } from "./surface.js";
9
+ import useWindowMoveHandle from "./use-window-move-handle.js";
9
10
  const HeaderContext = createContext(null);
10
11
  function useHeader() {
11
12
  const value = useContext(HeaderContext);
@@ -32,7 +33,7 @@ const WindowContent = forwardRef(function WindowContent({ style, ...properties }
32
33
  } });
33
34
  });
34
35
  /** The composable top region of a window; its enclosing Surface owns the material. */
35
- const HeaderRoot = forwardRef(function HeaderRoot({ active = true, children, color, onPointerDown, style, ...properties }, ref) {
36
+ const HeaderRoot = forwardRef(function HeaderRoot({ active = true, beginMoveGesture, children, color, onLostPointerCapture, onMoveError, onPointerCancel, onPointerDown, onPointerMove, onPointerUp, style, ...properties }, ref) {
36
37
  const resolved = useResolvedAppearance();
37
38
  const spacing = resolved.appearance.spacing;
38
39
  const foreground = style?.color ?? resolveColor(color ?? "foreground:base", resolved.colors);
@@ -43,7 +44,12 @@ const HeaderRoot = forwardRef(function HeaderRoot({ active = true, children, col
43
44
  spacing,
44
45
  transition: transitionTiming(resolved.transaction, resolved.preferences.animations)
45
46
  };
46
- return _jsx(HeaderContext.Provider, { value: state, children: _jsx("div", { ...properties, ref: ref, onPointerDown: onPointerDown, style: {
47
+ const move = useWindowMoveHandle(beginMoveGesture, onMoveError);
48
+ return _jsx(HeaderContext.Provider, { value: state, children: _jsx("div", { ...properties, ref: ref, onPointerDown: event => {
49
+ onPointerDown?.(event);
50
+ if (!event.defaultPrevented)
51
+ move.onPointerDown(event);
52
+ }, onPointerMove: event => { onPointerMove?.(event); move.onPointerMove(event); }, onPointerUp: event => { onPointerUp?.(event); move.onPointerUp(event); }, onPointerCancel: event => { onPointerCancel?.(event); move.onPointerCancel(event); }, onLostPointerCapture: event => { onLostPointerCapture?.(event); move.onLostPointerCapture(event); }, style: {
47
53
  boxSizing: "border-box",
48
54
  display: "flex",
49
55
  alignItems: "center",
@@ -53,8 +59,8 @@ const HeaderRoot = forwardRef(function HeaderRoot({ active = true, children, col
53
59
  paddingInline: spacing,
54
60
  flexShrink: 0,
55
61
  userSelect: "none",
56
- touchAction: onPointerDown ? "none" : undefined,
57
- cursor: onPointerDown ? "grab" : undefined,
62
+ touchAction: beginMoveGesture ? "none" : undefined,
63
+ cursor: beginMoveGesture ? "grab" : undefined,
58
64
  color: foreground,
59
65
  ...style
60
66
  }, children: children }) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
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.56",
60
+ "@phreshos/core": "^0.1.59",
61
61
  "colorjs.io": "^0.7.1",
62
62
  "motion": "^13.2.0",
63
63
  "react-aria": "^3.51.0",