@phreshos/react-ui 0.1.32 → 0.1.34

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/color.d.ts CHANGED
@@ -14,6 +14,8 @@ export declare function color(value: string): ColorScale;
14
14
  export declare function resolveColorLevel(value: string, level: ColorLevel): string;
15
15
  /** Returns the complete visual treatments derived from one concrete color. */
16
16
  export declare function useColor(value: string): ColorScale;
17
+ /** Returns the readable Appearance content color for one semantic or CSS fill. */
18
+ export declare function useContrastingColor(value: Color): string;
17
19
  /** Resolves a semantic color from an already selected Appearance branch. */
18
20
  export declare function resolveColor(value: Color | undefined, colors: AppearanceColors): string;
19
21
  /** Resolves opaque control paint from an already selected Appearance branch. */
package/dist/color.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from "react";
2
+ import { useResolvedAppearance } from "./appearance-context.js";
2
3
  import ColorSpace from "colorjs.io/src/ColorSpace.js";
3
4
  import { mix } from "colorjs.io/src/interpolation.js";
4
5
  import parse from "colorjs.io/src/parse.js";
@@ -61,6 +62,11 @@ export function resolveColorLevel(value, level) {
61
62
  export function useColor(value) {
62
63
  return useMemo(() => color(value), [value]);
63
64
  }
65
+ /** Returns the readable Appearance content color for one semantic or CSS fill. */
66
+ export function useContrastingColor(value) {
67
+ const { colors } = useResolvedAppearance();
68
+ return useMemo(() => contrastingColor(value, colors), [value, colors]);
69
+ }
64
70
  /** Resolves a semantic color from an already selected Appearance branch. */
65
71
  export function resolveColor(value, colors) {
66
72
  const resolved = value ?? defaultColor;
package/dist/main.d.ts CHANGED
@@ -13,7 +13,8 @@ export { Grid, type GridProps } from "./grid.js";
13
13
  export { Surface, type MaterialMode, type MaterialOptions, type ShadowOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
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
- 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";
16
+ 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";
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";
@@ -50,5 +51,5 @@ export type { ControlColor } from "./control.js";
50
51
  export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
51
52
  export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
52
53
  export { resolveSpacing, type Spacing } from "./spacing.js";
53
- export { useColor } from "./color.js";
54
+ export { useColor, useContrastingColor } from "./color.js";
54
55
  export { useScale, type NumericScale, type ScaleLevel } from "./scale.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";
@@ -46,5 +47,5 @@ export { AlertDialog } from "./alert-dialog.js";
46
47
  export { Tooltip } from "./tooltip.js";
47
48
  export { resolveRadius } from "./radius.js";
48
49
  export { resolveSpacing } from "./spacing.js";
49
- export { useColor } from "./color.js";
50
+ export { useColor, useContrastingColor } from "./color.js";
50
51
  export { useScale } from "./scale.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,14 @@ 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
+ /** Whether the represented window is currently maximized. */
18
+ readonly maximized?: boolean;
19
+ /** Toggles the represented window between maximized and restored states. */
20
+ readonly onMaximize?: () => void;
21
+ /** Receives a synchronous or asynchronous move handoff failure. */
22
+ readonly onMoveError?: (error: unknown) => void;
14
23
  }
15
24
  export interface WindowIdentityProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
16
25
  readonly icon?: string;
@@ -24,6 +33,7 @@ export interface WindowMaximizeProps extends WindowControlProps {
24
33
  readonly maximized?: boolean;
25
34
  }
26
35
  export type WindowCloseProps = WindowControlProps;
36
+ export type WindowMoveCaptureProps = HTMLAttributes<HTMLDivElement>;
27
37
  /** One Surface with a ready-to-use header-and-content composition. */
28
38
  export declare const Window: import("react").ForwardRefExoticComponent<Omit<WindowProps, "ref"> & import("react").RefAttributes<HTMLDivElement>> & {
29
39
  Header: import("react").ForwardRefExoticComponent<WindowHeaderProps & import("react").RefAttributes<HTMLDivElement>> & {
@@ -36,4 +46,5 @@ export declare const Window: import("react").ForwardRefExoticComponent<Omit<Wind
36
46
  Close: import("react").ForwardRefExoticComponent<WindowControlProps & import("react").RefAttributes<HTMLButtonElement>>;
37
47
  };
38
48
  Content: import("react").ForwardRefExoticComponent<WindowContentProps & import("react").RefAttributes<HTMLDivElement>>;
49
+ MoveCapture: import("react").ForwardRefExoticComponent<WindowMoveCaptureProps & import("react").RefAttributes<HTMLDivElement>>;
39
50
  };
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, maximized = false, onDoubleClick, onLostPointerCapture, onMaximize, 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);
@@ -40,10 +41,21 @@ const HeaderRoot = forwardRef(function HeaderRoot({ active = true, children, col
40
41
  active,
41
42
  foreground,
42
43
  iconRadius: scale(resolved.appearance.radius, "xsmall"),
44
+ maximized,
45
+ onMaximize,
43
46
  spacing,
44
47
  transition: transitionTiming(resolved.transaction, resolved.preferences.animations)
45
48
  };
46
- return _jsx(HeaderContext.Provider, { value: state, children: _jsx("div", { ...properties, ref: ref, onPointerDown: onPointerDown, style: {
49
+ const move = useWindowMoveHandle(beginMoveGesture, onMoveError);
50
+ return _jsx(HeaderContext.Provider, { value: state, children: _jsx("div", { ...properties, ref: ref, onPointerDown: event => {
51
+ onPointerDown?.(event);
52
+ if (!event.defaultPrevented)
53
+ move.onPointerDown(event);
54
+ }, 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); }, onDoubleClick: event => {
55
+ onDoubleClick?.(event);
56
+ if (!event.defaultPrevented)
57
+ onMaximize?.();
58
+ }, style: {
47
59
  boxSizing: "border-box",
48
60
  display: "flex",
49
61
  alignItems: "center",
@@ -53,8 +65,8 @@ const HeaderRoot = forwardRef(function HeaderRoot({ active = true, children, col
53
65
  paddingInline: spacing,
54
66
  flexShrink: 0,
55
67
  userSelect: "none",
56
- touchAction: onPointerDown ? "none" : undefined,
57
- cursor: onPointerDown ? "grab" : undefined,
68
+ touchAction: beginMoveGesture ? "none" : undefined,
69
+ cursor: beginMoveGesture ? "grab" : undefined,
58
70
  color: foreground,
59
71
  ...style
60
72
  }, children: children }) });
@@ -112,8 +124,10 @@ const WindowAction = forwardRef(function WindowAction({ ...properties }, ref) {
112
124
  const WindowMinimize = forwardRef(function WindowMinimize({ "aria-label": label = "Minimize", preventFocusOnPress = true, ...properties }, ref) {
113
125
  return _jsx(WindowAction, { ...properties, ref: ref, "aria-label": label, preventFocusOnPress: preventFocusOnPress, children: _jsx(ControlIcon, { children: _jsx("path", { d: "M1.5 7.5h7" }) }) });
114
126
  });
115
- const WindowMaximize = forwardRef(function WindowMaximize({ "aria-label": label, maximized = false, ...properties }, ref) {
116
- return _jsx(WindowAction, { ...properties, ref: ref, "aria-label": label ?? (maximized ? "Restore" : "Maximize"), children: _jsx(ControlIcon, { children: maximized
127
+ const WindowMaximize = forwardRef(function WindowMaximize({ "aria-label": label, maximized, onPress, ...properties }, ref) {
128
+ const header = useHeader();
129
+ const selected = maximized ?? header.maximized;
130
+ return _jsx(WindowAction, { ...properties, ref: ref, onPress: onPress ?? header.onMaximize, "aria-label": label ?? (selected ? "Restore" : "Maximize"), children: _jsx(ControlIcon, { children: selected
117
131
  ? _jsx("path", { d: "M1 4h5v5H1zM4 1h5v5H6.5", strokeWidth: "1.3", strokeLinejoin: "round" })
118
132
  : _jsx("path", { d: "M1.5 1.5h7v7h-7z", strokeWidth: "1.3", strokeLinejoin: "round" }) }) });
119
133
  });
@@ -132,8 +146,20 @@ const Header = Object.assign(HeaderRoot, {
132
146
  Maximize: WindowMaximize,
133
147
  Close: WindowClose
134
148
  });
149
+ /** The active full-viewport side of a Window move handoff. */
150
+ const WindowMoveCapture = forwardRef(function WindowMoveCapture({ style, ...properties }, ref) {
151
+ return _jsx("div", { ...properties, ref: ref, style: {
152
+ position: "fixed",
153
+ inset: 0,
154
+ touchAction: "none",
155
+ userSelect: "none",
156
+ cursor: "grab",
157
+ ...style
158
+ } });
159
+ });
135
160
  /** One Surface with a ready-to-use header-and-content composition. */
136
161
  export const Window = Object.assign(WindowRoot, {
137
162
  Header,
138
- Content: WindowContent
163
+ Content: WindowContent,
164
+ MoveCapture: WindowMoveCapture
139
165
  });
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.34",
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",