@tearleads/windowing 0.1.1
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/README.md +155 -0
- package/createRequiredContext.d.ts +16 -0
- package/createRequiredContext.js +22 -0
- package/createRequiredContext.js.map +1 -0
- package/index.d.ts +13 -0
- package/index.js +17 -0
- package/index.js.map +1 -0
- package/menu/Menu.css +105 -0
- package/menu/Menu.d.ts +11 -0
- package/menu/Menu.js +175 -0
- package/menu/Menu.js.map +1 -0
- package/menu/MenuItem.d.ts +9 -0
- package/menu/MenuItem.js +5 -0
- package/menu/MenuItem.js.map +1 -0
- package/menu/useContextMenuState.d.ts +20 -0
- package/menu/useContextMenuState.js +45 -0
- package/menu/useContextMenuState.js.map +1 -0
- package/menu/useMenuKeyboard.d.ts +2 -0
- package/menu/useMenuKeyboard.js +90 -0
- package/menu/useMenuKeyboard.js.map +1 -0
- package/package.json +27 -0
- package/sidebar/SidebarResize.css +31 -0
- package/sidebar/SidebarResize.d.ts +18 -0
- package/sidebar/SidebarResize.js +115 -0
- package/sidebar/SidebarResize.js.map +1 -0
- package/tokens.css +63 -0
- package/window/CurrentWindowContext.d.ts +27 -0
- package/window/CurrentWindowContext.js +31 -0
- package/window/CurrentWindowContext.js.map +1 -0
- package/window/Window.css +32 -0
- package/window/Window.d.ts +11 -0
- package/window/Window.js +206 -0
- package/window/Window.js.map +1 -0
- package/window/WindowBody.css +21 -0
- package/window/WindowBody.d.ts +8 -0
- package/window/WindowBody.js +10 -0
- package/window/WindowBody.js.map +1 -0
- package/window/WindowChromeActions.d.ts +55 -0
- package/window/WindowChromeActions.js +61 -0
- package/window/WindowChromeActions.js.map +1 -0
- package/window/WindowCloseButton.d.ts +6 -0
- package/window/WindowCloseButton.js +9 -0
- package/window/WindowCloseButton.js.map +1 -0
- package/window/WindowMaximizeButton.d.ts +3 -0
- package/window/WindowMaximizeButton.js +5 -0
- package/window/WindowMaximizeButton.js.map +1 -0
- package/window/WindowMenuBar.css +63 -0
- package/window/WindowMenuBar.d.ts +14 -0
- package/window/WindowMenuBar.js +37 -0
- package/window/WindowMenuBar.js.map +1 -0
- package/window/WindowMenuContext.d.ts +46 -0
- package/window/WindowMenuContext.js +208 -0
- package/window/WindowMenuContext.js.map +1 -0
- package/window/WindowMinimizeButton.d.ts +3 -0
- package/window/WindowMinimizeButton.js +5 -0
- package/window/WindowMinimizeButton.js.map +1 -0
- package/window/WindowResizeHandle.css +65 -0
- package/window/WindowResizeHandle.d.ts +5 -0
- package/window/WindowResizeHandle.js +6 -0
- package/window/WindowResizeHandle.js.map +1 -0
- package/window/WindowSidebar.css +37 -0
- package/window/WindowSidebar.d.ts +6 -0
- package/window/WindowSidebar.js +9 -0
- package/window/WindowSidebar.js.map +1 -0
- package/window/WindowSidebarContext.d.ts +14 -0
- package/window/WindowSidebarContext.js +43 -0
- package/window/WindowSidebarContext.js.map +1 -0
- package/window/WindowStateProvider/WindowStateProvider.d.ts +2 -0
- package/window/WindowStateProvider/WindowStateProvider.js +16 -0
- package/window/WindowStateProvider/WindowStateProvider.js.map +1 -0
- package/window/WindowStateProvider/context.d.ts +4 -0
- package/window/WindowStateProvider/context.js +7 -0
- package/window/WindowStateProvider/context.js.map +1 -0
- package/window/WindowStateProvider/index.d.ts +5 -0
- package/window/WindowStateProvider/index.js +5 -0
- package/window/WindowStateProvider/index.js.map +1 -0
- package/window/WindowStateProvider/types.d.ts +58 -0
- package/window/WindowStateProvider/types.js +2 -0
- package/window/WindowStateProvider/types.js.map +1 -0
- package/window/WindowStateProvider/useWindowActions.d.ts +1 -0
- package/window/WindowStateProvider/useWindowActions.js +3 -0
- package/window/WindowStateProvider/useWindowActions.js.map +1 -0
- package/window/WindowStateProvider/useWindowStateActions.d.ts +24 -0
- package/window/WindowStateProvider/useWindowStateActions.js +176 -0
- package/window/WindowStateProvider/useWindowStateActions.js.map +1 -0
- package/window/WindowStateProvider/useWindowStateData.d.ts +1 -0
- package/window/WindowStateProvider/useWindowStateData.js +3 -0
- package/window/WindowStateProvider/useWindowStateData.js.map +1 -0
- package/window/WindowStateProvider/util/bringWindowToFront.d.ts +2 -0
- package/window/WindowStateProvider/util/bringWindowToFront.js +21 -0
- package/window/WindowStateProvider/util/bringWindowToFront.js.map +1 -0
- package/window/WindowStateProvider/util/createWindowEntry.d.ts +3 -0
- package/window/WindowStateProvider/util/createWindowEntry.js +20 -0
- package/window/WindowStateProvider/util/createWindowEntry.js.map +1 -0
- package/window/WindowStateProvider/util/findAdjacentWindow.d.ts +2 -0
- package/window/WindowStateProvider/util/findAdjacentWindow.js +22 -0
- package/window/WindowStateProvider/util/findAdjacentWindow.js.map +1 -0
- package/window/WindowStateProvider/util/findTopWindow.d.ts +3 -0
- package/window/WindowStateProvider/util/findTopWindow.js +7 -0
- package/window/WindowStateProvider/util/findTopWindow.js.map +1 -0
- package/window/WindowStateProvider/util/getMaxWindowZIndex.d.ts +2 -0
- package/window/WindowStateProvider/util/getMaxWindowZIndex.js +4 -0
- package/window/WindowStateProvider/util/getMaxWindowZIndex.js.map +1 -0
- package/window/WindowStateProvider/util/index.d.ts +6 -0
- package/window/WindowStateProvider/util/index.js +7 -0
- package/window/WindowStateProvider/util/index.js.map +1 -0
- package/window/WindowStateProvider/util/swapWindowZIndexes.d.ts +2 -0
- package/window/WindowStateProvider/util/swapWindowZIndexes.js +21 -0
- package/window/WindowStateProvider/util/swapWindowZIndexes.js.map +1 -0
- package/window/WindowStateProvider/util/updateWindowFlag.d.ts +2 -0
- package/window/WindowStateProvider/util/updateWindowFlag.js +4 -0
- package/window/WindowStateProvider/util/updateWindowFlag.js.map +1 -0
- package/window/WindowStatusBar.css +40 -0
- package/window/WindowStatusBar.d.ts +4 -0
- package/window/WindowStatusBar.js +20 -0
- package/window/WindowStatusBar.js.map +1 -0
- package/window/WindowTitleBar.css +88 -0
- package/window/WindowTitleBar.d.ts +18 -0
- package/window/WindowTitleBar.js +36 -0
- package/window/WindowTitleBar.js.map +1 -0
- package/window/WindowToolBar.css +75 -0
- package/window/WindowToolBar.d.ts +38 -0
- package/window/WindowToolBar.js +68 -0
- package/window/WindowToolBar.js.map +1 -0
- package/window/useWindowGeometry.d.ts +16 -0
- package/window/useWindowGeometry.js +243 -0
- package/window/useWindowGeometry.js.map +1 -0
- package/window/useWindowItemRegistration.d.ts +10 -0
- package/window/useWindowItemRegistration.js +46 -0
- package/window/useWindowItemRegistration.js.map +1 -0
- package/window/useWindowItemRegistry.d.ts +12 -0
- package/window/useWindowItemRegistry.js +45 -0
- package/window/useWindowItemRegistry.js.map +1 -0
- package/window/useWindowKeyboardGeometry.d.ts +21 -0
- package/window/useWindowKeyboardGeometry.js +139 -0
- package/window/useWindowKeyboardGeometry.js.map +1 -0
- package/window/useWindowLayout.d.ts +18 -0
- package/window/useWindowLayout.js +115 -0
- package/window/useWindowLayout.js.map +1 -0
- package/window/useWindowToolbarReservationRegistry.d.ts +8 -0
- package/window/useWindowToolbarReservationRegistry.js +19 -0
- package/window/useWindowToolbarReservationRegistry.js.map +1 -0
- package/window/windowGeometry.d.ts +36 -0
- package/window/windowGeometry.js +54 -0
- package/window/windowGeometry.js.map +1 -0
- package/window/windowMenuRegistrations.d.ts +18 -0
- package/window/windowMenuRegistrations.js +34 -0
- package/window/windowMenuRegistrations.js.map +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.window-titlebar {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
gap: var(--space-sm);
|
|
6
|
+
min-height: var(--window-bar-height);
|
|
7
|
+
padding: var(--window-bar-padding);
|
|
8
|
+
background: var(--tearleads-window-titlebar-background);
|
|
9
|
+
color: var(--tearleads-window-titlebar-text);
|
|
10
|
+
line-height: var(--window-bar-line-height);
|
|
11
|
+
cursor: grab;
|
|
12
|
+
user-select: none;
|
|
13
|
+
/* Dragging is pointer-driven; keep a touch drag from scrolling the page. */
|
|
14
|
+
touch-action: none;
|
|
15
|
+
border-radius: var(--app-radius-surface) var(--app-radius-surface) 0 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.window-titlebar-title {
|
|
19
|
+
min-width: 0;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
text-overflow: ellipsis;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.window-titlebar:active {
|
|
27
|
+
cursor: grabbing;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.window--maximized .window-titlebar {
|
|
31
|
+
cursor: default;
|
|
32
|
+
border-radius: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.window-titlebar-buttons {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex: 0 0 auto;
|
|
38
|
+
gap: var(--space-xs);
|
|
39
|
+
/* Sit above the corner resize handles, which are absolutely positioned and
|
|
40
|
+
rendered after the titlebar, so they otherwise paint over this group and
|
|
41
|
+
the top-right (--ne) handle's hit target swallows clicks on the close
|
|
42
|
+
button. The handles stay transparent, so their hover bracket still shows
|
|
43
|
+
through; only the pointer priority in the button overlap goes to the
|
|
44
|
+
buttons. */
|
|
45
|
+
position: relative;
|
|
46
|
+
z-index: 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.window-close,
|
|
50
|
+
.window-maximize,
|
|
51
|
+
.window-minimize {
|
|
52
|
+
/* Interactive title controls get a larger hit area than the informational
|
|
53
|
+
status/menu bars; keep their compact shared bar token unchanged. */
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
min-width: 28px;
|
|
58
|
+
min-height: 28px;
|
|
59
|
+
padding: 0 var(--space-xs);
|
|
60
|
+
border: none;
|
|
61
|
+
background: none;
|
|
62
|
+
color: var(--tearleads-window-titlebar-text);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
line-height: 1;
|
|
65
|
+
border-radius: var(--app-radius-control);
|
|
66
|
+
transition: background var(--transition);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.window-close:hover,
|
|
70
|
+
.window-maximize:hover,
|
|
71
|
+
.window-minimize:hover,
|
|
72
|
+
.window-close:focus-visible,
|
|
73
|
+
.window-maximize:focus-visible,
|
|
74
|
+
.window-minimize:focus-visible {
|
|
75
|
+
background: color-mix(in srgb, currentColor 18%, transparent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.window-close:focus-visible,
|
|
79
|
+
.window-maximize:focus-visible,
|
|
80
|
+
.window-minimize:focus-visible {
|
|
81
|
+
outline: 2px solid currentColor;
|
|
82
|
+
outline-offset: -3px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.window-close:hover {
|
|
86
|
+
background: var(--color-danger);
|
|
87
|
+
color: var(--color-light);
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface WindowTitleBarAction {
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function WindowTitleBar({ title, titleId, onPointerDown, onMinimize, onMaximize, onClose, onMoveForward, onMoveBackward, }: {
|
|
10
|
+
title: string;
|
|
11
|
+
titleId?: string | undefined;
|
|
12
|
+
onPointerDown: (e: React.PointerEvent) => void;
|
|
13
|
+
onMinimize: () => void;
|
|
14
|
+
onMaximize: () => void;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
onMoveForward: () => void;
|
|
17
|
+
onMoveBackward: () => void;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowDownIcon } from "@phosphor-icons/react/dist/csr/ArrowDown";
|
|
3
|
+
import { ArrowUpIcon } from "@phosphor-icons/react/dist/csr/ArrowUp";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
import { Menu } from "../menu/Menu.js";
|
|
6
|
+
import { MenuItem } from "../menu/MenuItem.js";
|
|
7
|
+
import { useContextMenuPositionState } from "../menu/useContextMenuState.js";
|
|
8
|
+
import "./WindowTitleBar.css";
|
|
9
|
+
import { WindowCloseButton } from "./WindowCloseButton.js";
|
|
10
|
+
import { WindowMaximizeButton } from "./WindowMaximizeButton.js";
|
|
11
|
+
import { WindowMinimizeButton } from "./WindowMinimizeButton.js";
|
|
12
|
+
export function WindowTitleBar({ title, titleId, onPointerDown, onMinimize, onMaximize, onClose, onMoveForward, onMoveBackward, }) {
|
|
13
|
+
const { closeContextMenu, contextMenu, openContextMenuAt } = useContextMenuPositionState();
|
|
14
|
+
const handlePointerDown = useCallback((e) => {
|
|
15
|
+
const target = e.target;
|
|
16
|
+
if (e.button !== 0)
|
|
17
|
+
return;
|
|
18
|
+
// Element, not HTMLElement: an icon inside a button (including the
|
|
19
|
+
// portaled context menu, whose events bubble through React) is SVG.
|
|
20
|
+
if (target instanceof Element && target.closest("button"))
|
|
21
|
+
return;
|
|
22
|
+
onPointerDown(e);
|
|
23
|
+
}, [onPointerDown]);
|
|
24
|
+
return (_jsxs("div", { role: "toolbar", "aria-label": "Window controls", className: "window-titlebar", onPointerDown: handlePointerDown, onContextMenu: (event) => {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
event.stopPropagation();
|
|
27
|
+
openContextMenuAt({ x: event.clientX, y: event.clientY });
|
|
28
|
+
}, children: [_jsx("span", { className: "window-titlebar-title", id: titleId, title: title, children: title }), _jsxs("div", { className: "window-titlebar-buttons", children: [_jsx(WindowMinimizeButton, { onClick: onMinimize }), _jsx(WindowMaximizeButton, { onClick: onMaximize }), _jsx(WindowCloseButton, { label: "Close window", onClick: onClose })] }), contextMenu && (_jsxs(Menu, { position: contextMenu, onClose: closeContextMenu, direction: "down", children: [_jsx(MenuItem, { icon: ArrowUpIcon, label: "Move Forward", onClick: () => {
|
|
29
|
+
onMoveForward();
|
|
30
|
+
closeContextMenu();
|
|
31
|
+
} }), _jsx(MenuItem, { icon: ArrowDownIcon, label: "Move Backward", onClick: () => {
|
|
32
|
+
onMoveBackward();
|
|
33
|
+
closeContextMenu();
|
|
34
|
+
} })] }))] }));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=WindowTitleBar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WindowTitleBar.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/window/WindowTitleBar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAC;AACrE,OAAO,EAAkB,WAAW,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAU9D,MAAM,UAAU,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,EACP,aAAa,EACb,UAAU,EACV,UAAU,EACV,OAAO,EACP,aAAa,EACb,cAAc,GAUf;IACC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,GACxD,2BAA2B,EAAE,CAAC;IAEhC,MAAM,iBAAiB,GAAG,WAAW,CACnC,CAAC,CAAqC,EAAE,EAAE;QACxC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QACxB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC3B,mEAAmE;QACnE,oEAAoE;QACpE,IAAI,MAAM,YAAY,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,OAAO;QAClE,aAAa,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC,EACD,CAAC,aAAa,CAAC,CAChB,CAAC;IAEF,OAAO,CACL,eACE,IAAI,EAAC,SAAS,gBACH,iBAAiB,EAC5B,SAAS,EAAC,iBAAiB,EAC3B,aAAa,EAAE,iBAAiB,EAChC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;YACvB,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,eAAe,EAAE,CAAC;YACxB,iBAAiB,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,CAAC,aAED,eAAM,SAAS,EAAC,uBAAuB,EAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,YAC9D,KAAK,GACD,EACP,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,oBAAoB,IAAC,OAAO,EAAE,UAAU,GAAI,EAC7C,KAAC,oBAAoB,IAAC,OAAO,EAAE,UAAU,GAAI,EAC7C,KAAC,iBAAiB,IAAC,KAAK,EAAC,cAAc,EAAC,OAAO,EAAE,OAAO,GAAI,IACxD,EACL,WAAW,IAAI,CACd,MAAC,IAAI,IACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAC,MAAM,aAEhB,KAAC,QAAQ,IACP,IAAI,EAAE,WAAW,EACjB,KAAK,EAAC,cAAc,EACpB,OAAO,EAAE,GAAG,EAAE;4BACZ,aAAa,EAAE,CAAC;4BAChB,gBAAgB,EAAE,CAAC;wBACrB,CAAC,GACD,EACF,KAAC,QAAQ,IACP,IAAI,EAAE,aAAa,EACnB,KAAK,EAAC,eAAe,EACrB,OAAO,EAAE,GAAG,EAAE;4BACZ,cAAc,EAAE,CAAC;4BACjB,gBAAgB,EAAE,CAAC;wBACrB,CAAC,GACD,IACG,CACR,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { ArrowDownIcon } from \"@phosphor-icons/react/dist/csr/ArrowDown\";\nimport { ArrowUpIcon } from \"@phosphor-icons/react/dist/csr/ArrowUp\";\nimport { type ReactNode, useCallback } from \"react\";\nimport { Menu } from \"../menu/Menu\";\nimport { MenuItem } from \"../menu/MenuItem\";\nimport { useContextMenuPositionState } from \"../menu/useContextMenuState\";\nimport \"./WindowTitleBar.css\";\nimport { WindowCloseButton } from \"./WindowCloseButton\";\nimport { WindowMaximizeButton } from \"./WindowMaximizeButton\";\nimport { WindowMinimizeButton } from \"./WindowMinimizeButton\";\n\nexport interface WindowTitleBarAction {\n disabled?: boolean;\n icon: ReactNode;\n id: string;\n label: string;\n onClick: () => void;\n}\n\nexport function WindowTitleBar({\n title,\n titleId,\n onPointerDown,\n onMinimize,\n onMaximize,\n onClose,\n onMoveForward,\n onMoveBackward,\n}: {\n title: string;\n titleId?: string | undefined;\n onPointerDown: (e: React.PointerEvent) => void;\n onMinimize: () => void;\n onMaximize: () => void;\n onClose: () => void;\n onMoveForward: () => void;\n onMoveBackward: () => void;\n}) {\n const { closeContextMenu, contextMenu, openContextMenuAt } =\n useContextMenuPositionState();\n\n const handlePointerDown = useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n const target = e.target;\n if (e.button !== 0) return;\n // Element, not HTMLElement: an icon inside a button (including the\n // portaled context menu, whose events bubble through React) is SVG.\n if (target instanceof Element && target.closest(\"button\")) return;\n onPointerDown(e);\n },\n [onPointerDown],\n );\n\n return (\n <div\n role=\"toolbar\"\n aria-label=\"Window controls\"\n className=\"window-titlebar\"\n onPointerDown={handlePointerDown}\n onContextMenu={(event) => {\n event.preventDefault();\n event.stopPropagation();\n openContextMenuAt({ x: event.clientX, y: event.clientY });\n }}\n >\n <span className=\"window-titlebar-title\" id={titleId} title={title}>\n {title}\n </span>\n <div className=\"window-titlebar-buttons\">\n <WindowMinimizeButton onClick={onMinimize} />\n <WindowMaximizeButton onClick={onMaximize} />\n <WindowCloseButton label=\"Close window\" onClick={onClose} />\n </div>\n {contextMenu && (\n <Menu\n position={contextMenu}\n onClose={closeContextMenu}\n direction=\"down\"\n >\n <MenuItem\n icon={ArrowUpIcon}\n label=\"Move Forward\"\n onClick={() => {\n onMoveForward();\n closeContextMenu();\n }}\n />\n <MenuItem\n icon={ArrowDownIcon}\n label=\"Move Backward\"\n onClick={() => {\n onMoveBackward();\n closeContextMenu();\n }}\n />\n </Menu>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.window-toolbar {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--space-2xs);
|
|
5
|
+
/*
|
|
6
|
+
* Reserve an ICON-INCLUSIVE row height so an EMPTY toolbar is exactly as tall
|
|
7
|
+
* as one holding icon buttons. An action-less sub-route (e.g. a container's
|
|
8
|
+
* "Get Info" view registers no toolbar actions and no back action) renders the
|
|
9
|
+
* bar with only a spacer, collapsing it to its min-height. `--window-bar-height`
|
|
10
|
+
* only accounts for the text line box (1.25em + padding); when the inherited
|
|
11
|
+
* font size makes 1.25em smaller than the 18px action icons, a FILLED row is
|
|
12
|
+
* driven taller by the icons and exceeds that min-height. The empty bar then
|
|
13
|
+
* sits shorter, so returning to the filled view shifts the body up then down.
|
|
14
|
+
* Reserve the LARGER of the standard bar height and an icon-inclusive bar
|
|
15
|
+
* height (18px icon + the same block padding), so the empty and filled bar
|
|
16
|
+
* always clamp to the same height. Uses the literal 18px (not 1.125rem) because
|
|
17
|
+
* size={18} renders a fixed-pixel icon regardless of the root font size, so a
|
|
18
|
+
* rem here would drift out of sync under accessibility zoom or a custom root
|
|
19
|
+
* font and re-introduce the shift. Keep 18px == the icon size.
|
|
20
|
+
*/
|
|
21
|
+
min-height: max(
|
|
22
|
+
var(--window-bar-height),
|
|
23
|
+
calc(18px + var(--window-bar-block-padding) * 2)
|
|
24
|
+
);
|
|
25
|
+
background: var(--color-muted);
|
|
26
|
+
border-top: var(--border-width) solid var(--color-hairline);
|
|
27
|
+
user-select: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.window-toolbar-primary,
|
|
31
|
+
.window-toolbar-actions {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.window-toolbar-spacer {
|
|
38
|
+
flex: 1 1 auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.window-toolbar-button {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: var(--space-2xs);
|
|
45
|
+
min-width: 0;
|
|
46
|
+
padding: var(--window-bar-padding);
|
|
47
|
+
border: none;
|
|
48
|
+
background: none;
|
|
49
|
+
color: inherit;
|
|
50
|
+
font: inherit;
|
|
51
|
+
line-height: var(--window-bar-line-height);
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.window-toolbar-button:hover:not(:disabled),
|
|
56
|
+
.window-toolbar-button:focus-visible:not(:disabled) {
|
|
57
|
+
background: var(--emphasis-surface);
|
|
58
|
+
color: var(--emphasis-text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.window-toolbar-button:disabled {
|
|
62
|
+
cursor: default;
|
|
63
|
+
opacity: var(--opacity-disabled);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.window-toolbar-button svg {
|
|
67
|
+
flex: 0 0 auto;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.window-toolbar-back-label {
|
|
71
|
+
min-width: 0;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window's toolbar row, sitting below the File/View menu bar and above the
|
|
3
|
+
* body. It renders the same actions the routed app bar shows — the registered
|
|
4
|
+
* {@link useWindowBackAction} on the left and {@link useWindowTitleBarAction}
|
|
5
|
+
* icon buttons on the right — so a mini-app hosted in a window gets the same
|
|
6
|
+
* toolbar chrome it gets in the routed shell.
|
|
7
|
+
*
|
|
8
|
+
* The left slot resolves exactly as `RoutedPaneAppBar` does: a registered back
|
|
9
|
+
* action wins, and otherwise the button steps back through the host's own
|
|
10
|
+
* history. `showHistoryBack` turns that fallback on for a window hosting a
|
|
11
|
+
* routed mini-app, whose history is the window's Back stack (windows are not
|
|
12
|
+
* backed by browser history); the fallback is omitted when that stack is empty.
|
|
13
|
+
* Unlike the routed shell's paired Back/Forward controls, the window has no
|
|
14
|
+
* adjacent history control whose stable placement would justify a disabled
|
|
15
|
+
* Back button.
|
|
16
|
+
*
|
|
17
|
+
* Both surfaces read one shared registry (WindowMenuContext), so a mini-app
|
|
18
|
+
* registers its actions once and they appear wherever it is hosted.
|
|
19
|
+
*
|
|
20
|
+
* A window whose app never registers toolbar chrome keeps a flush menu bar/body
|
|
21
|
+
* (the row renders nothing). But once an app HAS shown chrome, the row stays
|
|
22
|
+
* mounted and reserves its {@link WindowToolBar.css} min-height even when the
|
|
23
|
+
* action set momentarily clears — on an action-less sub-route or a loading tick
|
|
24
|
+
* where the app's `show` conditions are briefly false. Unmounting the row in
|
|
25
|
+
* those windows would drop a full bar-height out of the flex column and shift
|
|
26
|
+
* the body up, then back down when the actions re-register (visible flicker).
|
|
27
|
+
*
|
|
28
|
+
* An app that wants the row on every route — a blank bar where a route carries
|
|
29
|
+
* no actions, rather than one that only appears after some route first shows
|
|
30
|
+
* chrome — reserves it via {@link useWindowToolbarReservation} (the reserved row
|
|
31
|
+
* still mounts a commit late, once that hook's effect registers, as all window
|
|
32
|
+
* chrome does).
|
|
33
|
+
*/
|
|
34
|
+
export declare function WindowToolBar({ canGoBack, onGoBack, showHistoryBack, }?: {
|
|
35
|
+
canGoBack?: boolean;
|
|
36
|
+
onGoBack?: (() => void) | undefined;
|
|
37
|
+
showHistoryBack?: boolean;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CaretLeftIcon } from "@phosphor-icons/react/dist/csr/CaretLeft";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import "./WindowToolBar.css";
|
|
5
|
+
import { WindowTitleBarActionButtons } from "./WindowChromeActions.js";
|
|
6
|
+
import { useWindowBackActionValue, useWindowTitleBarActions, useWindowToolbarReservationReleased, useWindowToolbarReserved, } from "./WindowMenuContext.js";
|
|
7
|
+
/**
|
|
8
|
+
* The window's toolbar row, sitting below the File/View menu bar and above the
|
|
9
|
+
* body. It renders the same actions the routed app bar shows — the registered
|
|
10
|
+
* {@link useWindowBackAction} on the left and {@link useWindowTitleBarAction}
|
|
11
|
+
* icon buttons on the right — so a mini-app hosted in a window gets the same
|
|
12
|
+
* toolbar chrome it gets in the routed shell.
|
|
13
|
+
*
|
|
14
|
+
* The left slot resolves exactly as `RoutedPaneAppBar` does: a registered back
|
|
15
|
+
* action wins, and otherwise the button steps back through the host's own
|
|
16
|
+
* history. `showHistoryBack` turns that fallback on for a window hosting a
|
|
17
|
+
* routed mini-app, whose history is the window's Back stack (windows are not
|
|
18
|
+
* backed by browser history); the fallback is omitted when that stack is empty.
|
|
19
|
+
* Unlike the routed shell's paired Back/Forward controls, the window has no
|
|
20
|
+
* adjacent history control whose stable placement would justify a disabled
|
|
21
|
+
* Back button.
|
|
22
|
+
*
|
|
23
|
+
* Both surfaces read one shared registry (WindowMenuContext), so a mini-app
|
|
24
|
+
* registers its actions once and they appear wherever it is hosted.
|
|
25
|
+
*
|
|
26
|
+
* A window whose app never registers toolbar chrome keeps a flush menu bar/body
|
|
27
|
+
* (the row renders nothing). But once an app HAS shown chrome, the row stays
|
|
28
|
+
* mounted and reserves its {@link WindowToolBar.css} min-height even when the
|
|
29
|
+
* action set momentarily clears — on an action-less sub-route or a loading tick
|
|
30
|
+
* where the app's `show` conditions are briefly false. Unmounting the row in
|
|
31
|
+
* those windows would drop a full bar-height out of the flex column and shift
|
|
32
|
+
* the body up, then back down when the actions re-register (visible flicker).
|
|
33
|
+
*
|
|
34
|
+
* An app that wants the row on every route — a blank bar where a route carries
|
|
35
|
+
* no actions, rather than one that only appears after some route first shows
|
|
36
|
+
* chrome — reserves it via {@link useWindowToolbarReservation} (the reserved row
|
|
37
|
+
* still mounts a commit late, once that hook's effect registers, as all window
|
|
38
|
+
* chrome does).
|
|
39
|
+
*/
|
|
40
|
+
export function WindowToolBar({ canGoBack = false, onGoBack, showHistoryBack = false, } = {}) {
|
|
41
|
+
const backAction = useWindowBackActionValue();
|
|
42
|
+
const actions = useWindowTitleBarActions();
|
|
43
|
+
const reserved = useWindowToolbarReserved();
|
|
44
|
+
const reservationReleased = useWindowToolbarReservationReleased();
|
|
45
|
+
const showBack = backAction !== null || (showHistoryBack && canGoBack);
|
|
46
|
+
const hasChrome = backAction !== null || showHistoryBack || actions.length > 0;
|
|
47
|
+
// Latch: this window's app has shown toolbar chrome at least once, so keep the
|
|
48
|
+
// row reserved from here on rather than collapsing it on a transient empty.
|
|
49
|
+
// Seed from the mount-time value so a window that already has chrome does not
|
|
50
|
+
// latch during render at all; the render-phase set below catches chrome that
|
|
51
|
+
// first appears after mount, without an effect's extra committed render pass.
|
|
52
|
+
const [hasShownChrome, setHasShownChrome] = useState(hasChrome);
|
|
53
|
+
let latchedChrome = hasShownChrome;
|
|
54
|
+
if (hasChrome && !latchedChrome) {
|
|
55
|
+
setHasShownChrome(true);
|
|
56
|
+
latchedChrome = true;
|
|
57
|
+
}
|
|
58
|
+
else if (!hasChrome && reservationReleased && latchedChrome) {
|
|
59
|
+
setHasShownChrome(false);
|
|
60
|
+
latchedChrome = false;
|
|
61
|
+
}
|
|
62
|
+
if (!hasChrome && !latchedChrome && !reserved) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const backLabel = backAction?.label ?? "Back";
|
|
66
|
+
return (_jsxs("div", { "aria-label": "Toolbar", className: "window-toolbar", role: "toolbar", children: [_jsx("div", { className: "window-toolbar-primary", children: showBack && (_jsxs("button", { "aria-label": backLabel, className: "window-toolbar-button", disabled: backAction?.disabled ?? false, title: backLabel, type: "button", onClick: backAction?.onClick ?? onGoBack, children: [_jsx(CaretLeftIcon, { "aria-hidden": true, size: 18 }), _jsx("span", { className: "window-toolbar-back-label", children: backLabel })] })) }), _jsx("div", { className: "window-toolbar-spacer" }), actions.length > 0 && (_jsx("div", { className: "window-toolbar-actions", children: _jsx(WindowTitleBarActionButtons, { actions: actions, className: "window-toolbar-button" }) }))] }));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=WindowToolBar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WindowToolBar.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/window/WindowToolBar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,qBAAqB,CAAC;AAC7B,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,mCAAmC,EACnC,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,SAAS,GAAG,KAAK,EACjB,QAAQ,EACR,eAAe,GAAG,KAAK,MAKrB,EAAE;IACJ,MAAM,UAAU,GAAG,wBAAwB,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,wBAAwB,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAG,wBAAwB,EAAE,CAAC;IAC5C,MAAM,mBAAmB,GAAG,mCAAmC,EAAE,CAAC;IAClE,MAAM,QAAQ,GAAG,UAAU,KAAK,IAAI,IAAI,CAAC,eAAe,IAAI,SAAS,CAAC,CAAC;IACvE,MAAM,SAAS,GACb,UAAU,KAAK,IAAI,IAAI,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/D,+EAA+E;IAC/E,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAChE,IAAI,aAAa,GAAG,cAAc,CAAC;IACnC,IAAI,SAAS,IAAI,CAAC,aAAa,EAAE,CAAC;QAChC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACxB,aAAa,GAAG,IAAI,CAAC;IACvB,CAAC;SAAM,IAAI,CAAC,SAAS,IAAI,mBAAmB,IAAI,aAAa,EAAE,CAAC;QAC9D,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACzB,aAAa,GAAG,KAAK,CAAC;IACxB,CAAC;IAED,IAAI,CAAC,SAAS,IAAI,CAAC,aAAa,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,EAAE,KAAK,IAAI,MAAM,CAAC;IAE9C,OAAO,CACL,6BAAgB,SAAS,EAAC,SAAS,EAAC,gBAAgB,EAAC,IAAI,EAAC,SAAS,aACjE,cAAK,SAAS,EAAC,wBAAwB,YACpC,QAAQ,IAAI,CACX,gCACc,SAAS,EACrB,SAAS,EAAC,uBAAuB,EACjC,QAAQ,EAAE,UAAU,EAAE,QAAQ,IAAI,KAAK,EACvC,KAAK,EAAE,SAAS,EAChB,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,UAAU,EAAE,OAAO,IAAI,QAAQ,aAExC,KAAC,aAAa,yBAAa,IAAI,EAAE,EAAE,GAAI,EACvC,eAAM,SAAS,EAAC,2BAA2B,YAAE,SAAS,GAAQ,IACvD,CACV,GACG,EACN,cAAK,SAAS,EAAC,uBAAuB,GAAG,EACxC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CACrB,cAAK,SAAS,EAAC,wBAAwB,YACrC,KAAC,2BAA2B,IAC1B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,uBAAuB,GACjC,GACE,CACP,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { CaretLeftIcon } from \"@phosphor-icons/react/dist/csr/CaretLeft\";\nimport { useState } from \"react\";\nimport \"./WindowToolBar.css\";\nimport { WindowTitleBarActionButtons } from \"./WindowChromeActions\";\nimport {\n useWindowBackActionValue,\n useWindowTitleBarActions,\n useWindowToolbarReservationReleased,\n useWindowToolbarReserved,\n} from \"./WindowMenuContext\";\n\n/**\n * The window's toolbar row, sitting below the File/View menu bar and above the\n * body. It renders the same actions the routed app bar shows — the registered\n * {@link useWindowBackAction} on the left and {@link useWindowTitleBarAction}\n * icon buttons on the right — so a mini-app hosted in a window gets the same\n * toolbar chrome it gets in the routed shell.\n *\n * The left slot resolves exactly as `RoutedPaneAppBar` does: a registered back\n * action wins, and otherwise the button steps back through the host's own\n * history. `showHistoryBack` turns that fallback on for a window hosting a\n * routed mini-app, whose history is the window's Back stack (windows are not\n * backed by browser history); the fallback is omitted when that stack is empty.\n * Unlike the routed shell's paired Back/Forward controls, the window has no\n * adjacent history control whose stable placement would justify a disabled\n * Back button.\n *\n * Both surfaces read one shared registry (WindowMenuContext), so a mini-app\n * registers its actions once and they appear wherever it is hosted.\n *\n * A window whose app never registers toolbar chrome keeps a flush menu bar/body\n * (the row renders nothing). But once an app HAS shown chrome, the row stays\n * mounted and reserves its {@link WindowToolBar.css} min-height even when the\n * action set momentarily clears — on an action-less sub-route or a loading tick\n * where the app's `show` conditions are briefly false. Unmounting the row in\n * those windows would drop a full bar-height out of the flex column and shift\n * the body up, then back down when the actions re-register (visible flicker).\n *\n * An app that wants the row on every route — a blank bar where a route carries\n * no actions, rather than one that only appears after some route first shows\n * chrome — reserves it via {@link useWindowToolbarReservation} (the reserved row\n * still mounts a commit late, once that hook's effect registers, as all window\n * chrome does).\n */\nexport function WindowToolBar({\n canGoBack = false,\n onGoBack,\n showHistoryBack = false,\n}: {\n canGoBack?: boolean;\n onGoBack?: (() => void) | undefined;\n showHistoryBack?: boolean;\n} = {}) {\n const backAction = useWindowBackActionValue();\n const actions = useWindowTitleBarActions();\n const reserved = useWindowToolbarReserved();\n const reservationReleased = useWindowToolbarReservationReleased();\n const showBack = backAction !== null || (showHistoryBack && canGoBack);\n const hasChrome =\n backAction !== null || showHistoryBack || actions.length > 0;\n // Latch: this window's app has shown toolbar chrome at least once, so keep the\n // row reserved from here on rather than collapsing it on a transient empty.\n // Seed from the mount-time value so a window that already has chrome does not\n // latch during render at all; the render-phase set below catches chrome that\n // first appears after mount, without an effect's extra committed render pass.\n const [hasShownChrome, setHasShownChrome] = useState(hasChrome);\n let latchedChrome = hasShownChrome;\n if (hasChrome && !latchedChrome) {\n setHasShownChrome(true);\n latchedChrome = true;\n } else if (!hasChrome && reservationReleased && latchedChrome) {\n setHasShownChrome(false);\n latchedChrome = false;\n }\n\n if (!hasChrome && !latchedChrome && !reserved) {\n return null;\n }\n\n const backLabel = backAction?.label ?? \"Back\";\n\n return (\n <div aria-label=\"Toolbar\" className=\"window-toolbar\" role=\"toolbar\">\n <div className=\"window-toolbar-primary\">\n {showBack && (\n <button\n aria-label={backLabel}\n className=\"window-toolbar-button\"\n disabled={backAction?.disabled ?? false}\n title={backLabel}\n type=\"button\"\n onClick={backAction?.onClick ?? onGoBack}\n >\n <CaretLeftIcon aria-hidden size={18} />\n <span className=\"window-toolbar-back-label\">{backLabel}</span>\n </button>\n )}\n </div>\n <div className=\"window-toolbar-spacer\" />\n {actions.length > 0 && (\n <div className=\"window-toolbar-actions\">\n <WindowTitleBarActionButtons\n actions={actions}\n className=\"window-toolbar-button\"\n />\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, type RefObject } from "react";
|
|
2
|
+
import type { ResizeCorner } from "./WindowResizeHandle.js";
|
|
3
|
+
import { type WindowEntry, type WindowPosition, type WindowSize } from "./WindowStateProvider/index.js";
|
|
4
|
+
import { type LiveGeometry } from "./windowGeometry.js";
|
|
5
|
+
export declare function useWindowGeometry(entry: WindowEntry, maximized: boolean, windowRef: RefObject<HTMLElement | null>): {
|
|
6
|
+
commit: () => void;
|
|
7
|
+
hold: (holding: boolean) => void;
|
|
8
|
+
handlePointerDown: (event: ReactPointerEvent) => void;
|
|
9
|
+
handleResizePointerDown: (event: ReactPointerEvent, corner: ResizeCorner) => void;
|
|
10
|
+
position: WindowPosition | null;
|
|
11
|
+
size: WindowSize | null;
|
|
12
|
+
grow: (deltaWidth: number, deltaHeight: number) => void;
|
|
13
|
+
nudge: (deltaX: number, deltaY: number) => void;
|
|
14
|
+
restore: (saved: LiveGeometry) => void;
|
|
15
|
+
snapshot: () => LiveGeometry;
|
|
16
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, } from "react";
|
|
2
|
+
import { useWindowLayout } from "./useWindowLayout.js";
|
|
3
|
+
import { useWindowActions, } from "./WindowStateProvider/index.js";
|
|
4
|
+
import { clampWindowPosition, MIN_HEIGHT, MIN_WIDTH, resizeWindowWithinContainer, } from "./windowGeometry.js";
|
|
5
|
+
// Pointer events cover mouse, touch, and pen alike. The live geometry stays in
|
|
6
|
+
// component state while a gesture runs, so only this window re-renders per
|
|
7
|
+
// frame; the gesture's end commits it to the shared window state once.
|
|
8
|
+
function useWindowPointerTracking(windowRef, dragging, resizing, clamp, setPosition, setSize, finish) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// Only the pointer that started a gesture drives it, so a second finger
|
|
11
|
+
// cannot move the window or end the drag.
|
|
12
|
+
function isGesturePointer(event) {
|
|
13
|
+
const gesture = resizing.current ?? dragging.current;
|
|
14
|
+
return gesture !== null && gesture.pointerId === event.pointerId;
|
|
15
|
+
}
|
|
16
|
+
function handlePointerMove(event) {
|
|
17
|
+
if (!isGesturePointer(event)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (resizing.current) {
|
|
21
|
+
const nextFrame = resizeWindowWithinContainer(resizing.current, event.clientX, event.clientY, windowRef.current?.parentElement ?? null);
|
|
22
|
+
setPosition(nextFrame.position);
|
|
23
|
+
setSize(nextFrame.size);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (dragging.current) {
|
|
27
|
+
setPosition(clamp(event.clientX - dragging.current.offsetX, event.clientY - dragging.current.offsetY));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function handlePointerEnd(event) {
|
|
31
|
+
if (!isGesturePointer(event)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
dragging.current = null;
|
|
35
|
+
resizing.current = null;
|
|
36
|
+
finish();
|
|
37
|
+
}
|
|
38
|
+
document.addEventListener("pointermove", handlePointerMove);
|
|
39
|
+
document.addEventListener("pointerup", handlePointerEnd);
|
|
40
|
+
document.addEventListener("pointercancel", handlePointerEnd);
|
|
41
|
+
document.addEventListener("lostpointercapture", handlePointerEnd);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener("pointermove", handlePointerMove);
|
|
44
|
+
document.removeEventListener("pointerup", handlePointerEnd);
|
|
45
|
+
document.removeEventListener("pointercancel", handlePointerEnd);
|
|
46
|
+
document.removeEventListener("lostpointercapture", handlePointerEnd);
|
|
47
|
+
};
|
|
48
|
+
}, [clamp, dragging, finish, resizing, setPosition, setSize, windowRef]);
|
|
49
|
+
}
|
|
50
|
+
function useLiveGeometry(entry) {
|
|
51
|
+
const { setGeometry } = useWindowActions();
|
|
52
|
+
const [position, setPositionState] = useState(null);
|
|
53
|
+
const [size, setSizeState] = useState(entry.size ?? null);
|
|
54
|
+
// Mirrors the live state so a gesture's end commits the latest frame.
|
|
55
|
+
const live = useRef({
|
|
56
|
+
position: null,
|
|
57
|
+
size: entry.size ?? null,
|
|
58
|
+
});
|
|
59
|
+
const setPosition = useCallback((value) => {
|
|
60
|
+
live.current.position = value;
|
|
61
|
+
setPositionState(value);
|
|
62
|
+
}, []);
|
|
63
|
+
const setSize = useCallback((value) => {
|
|
64
|
+
live.current.size = value;
|
|
65
|
+
setSizeState(value);
|
|
66
|
+
}, []);
|
|
67
|
+
const commit = useCallback(() => {
|
|
68
|
+
const committedPosition = live.current.position;
|
|
69
|
+
if (committedPosition) {
|
|
70
|
+
setGeometry(entry.id, {
|
|
71
|
+
position: committedPosition,
|
|
72
|
+
size: live.current.size ?? undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}, [entry.id, setGeometry]);
|
|
76
|
+
// The committed size is the source of truth between gestures: a restored
|
|
77
|
+
// layout or host call applies it, and clearing it restores the default size.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setSize(entry.size ?? null);
|
|
80
|
+
}, [entry.size, setSize]);
|
|
81
|
+
return { commit, live, position, setPosition, setSize, size };
|
|
82
|
+
}
|
|
83
|
+
// Stepwise geometry for keyboard move and resize. Steps stay local until the
|
|
84
|
+
// caller commits, like a pointer gesture; restore puts a snapshot back.
|
|
85
|
+
function useSteppedGeometry(windowRef, maximized, live, clamp, setPosition, setSize) {
|
|
86
|
+
const nudge = useCallback((deltaX, deltaY) => {
|
|
87
|
+
const current = live.current.position;
|
|
88
|
+
if (!current || maximized) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setPosition(clamp(current.x + deltaX, current.y + deltaY));
|
|
92
|
+
}, [clamp, live, maximized, setPosition]);
|
|
93
|
+
const grow = useCallback((deltaWidth, deltaHeight) => {
|
|
94
|
+
const element = windowRef.current;
|
|
95
|
+
const current = live.current.position;
|
|
96
|
+
if (!element || !current || maximized) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Step from the size on screen: a requested size below the stylesheet
|
|
100
|
+
// minimums renders at the minimum, so growing from it would show nothing.
|
|
101
|
+
const requested = live.current.size;
|
|
102
|
+
const base = requested
|
|
103
|
+
? {
|
|
104
|
+
height: Math.max(MIN_HEIGHT, requested.height),
|
|
105
|
+
width: Math.max(MIN_WIDTH, requested.width),
|
|
106
|
+
}
|
|
107
|
+
: { height: element.offsetHeight, width: element.offsetWidth };
|
|
108
|
+
const container = element.parentElement;
|
|
109
|
+
const maxWidth = container
|
|
110
|
+
? container.clientWidth - current.x
|
|
111
|
+
: Number.POSITIVE_INFINITY;
|
|
112
|
+
const maxHeight = container
|
|
113
|
+
? container.clientHeight - current.y
|
|
114
|
+
: Number.POSITIVE_INFINITY;
|
|
115
|
+
setSize({
|
|
116
|
+
height: Math.max(MIN_HEIGHT, Math.min(base.height + deltaHeight, maxHeight)),
|
|
117
|
+
width: Math.max(MIN_WIDTH, Math.min(base.width + deltaWidth, maxWidth)),
|
|
118
|
+
});
|
|
119
|
+
}, [live, maximized, setSize, windowRef]);
|
|
120
|
+
const snapshot = useCallback(() => ({ ...live.current }), [live]);
|
|
121
|
+
const restore = useCallback((saved) => {
|
|
122
|
+
if (saved.position) {
|
|
123
|
+
setPosition(saved.position);
|
|
124
|
+
}
|
|
125
|
+
setSize(saved.size);
|
|
126
|
+
}, [setPosition, setSize]);
|
|
127
|
+
return { grow, nudge, restore, snapshot };
|
|
128
|
+
}
|
|
129
|
+
// Keeps a gesture's pointer events coming to the window even when the pointer
|
|
130
|
+
// passes over an iframe, whose document would otherwise swallow the release.
|
|
131
|
+
// Synthetic events carry no active pointer, so a failed capture is fine.
|
|
132
|
+
function capturePointer(event) {
|
|
133
|
+
try {
|
|
134
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Not an active pointer; the document listeners still track the gesture.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Starts a drag from the title bar or a resize from a corner. The gesture
|
|
141
|
+
// itself is tracked on the document by useWindowPointerTracking.
|
|
142
|
+
function useGestureStarts(windowRef, maximized, position, dragging, resizing) {
|
|
143
|
+
const handlePointerDown = useCallback((event) => {
|
|
144
|
+
// One gesture at a time: a second pointer pressing the title bar or a
|
|
145
|
+
// corner mid-gesture must not take it over.
|
|
146
|
+
if (!position || maximized || dragging.current || resizing.current) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
dragging.current = {
|
|
150
|
+
offsetX: event.clientX - position.x,
|
|
151
|
+
offsetY: event.clientY - position.y,
|
|
152
|
+
pointerId: event.pointerId,
|
|
153
|
+
};
|
|
154
|
+
capturePointer(event);
|
|
155
|
+
}, [dragging, maximized, position, resizing]);
|
|
156
|
+
const handleResizePointerDown = useCallback((event, corner) => {
|
|
157
|
+
if (maximized ||
|
|
158
|
+
!position ||
|
|
159
|
+
!windowRef.current ||
|
|
160
|
+
dragging.current ||
|
|
161
|
+
resizing.current) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
capturePointer(event);
|
|
166
|
+
const el = windowRef.current;
|
|
167
|
+
const computed = getComputedStyle(el);
|
|
168
|
+
const borderBox = computed.boxSizing === "border-box";
|
|
169
|
+
resizing.current = {
|
|
170
|
+
corner,
|
|
171
|
+
pointerId: event.pointerId,
|
|
172
|
+
startX: event.clientX,
|
|
173
|
+
startY: event.clientY,
|
|
174
|
+
startLeft: position.x,
|
|
175
|
+
startTop: position.y,
|
|
176
|
+
startWidth: parseFloat(computed.width),
|
|
177
|
+
startHeight: parseFloat(computed.height),
|
|
178
|
+
borderX: borderBox ? 0 : el.offsetWidth - el.clientWidth,
|
|
179
|
+
borderY: borderBox ? 0 : el.offsetHeight - el.clientHeight,
|
|
180
|
+
};
|
|
181
|
+
}, [maximized, position, resizing, windowRef]);
|
|
182
|
+
return { handlePointerDown, handleResizePointerDown };
|
|
183
|
+
}
|
|
184
|
+
export function useWindowGeometry(entry, maximized, windowRef) {
|
|
185
|
+
const { commit, live, position, setPosition, setSize, size } = useLiveGeometry(entry);
|
|
186
|
+
const dragging = useRef(null);
|
|
187
|
+
const resizing = useRef(null);
|
|
188
|
+
// Maximizing or minimizing mid-gesture (a second touch on the title bar's
|
|
189
|
+
// controls) abandons the gesture: its frames were measured at the normal
|
|
190
|
+
// size, so the live geometry goes back to the committed geometry.
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!(maximized || entry.minimized)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!dragging.current && !resizing.current) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
dragging.current = null;
|
|
199
|
+
resizing.current = null;
|
|
200
|
+
setSize(entry.size ?? null);
|
|
201
|
+
if (entry.position) {
|
|
202
|
+
setPosition(entry.position);
|
|
203
|
+
}
|
|
204
|
+
}, [
|
|
205
|
+
entry.minimized,
|
|
206
|
+
entry.position,
|
|
207
|
+
entry.size,
|
|
208
|
+
maximized,
|
|
209
|
+
setPosition,
|
|
210
|
+
setSize,
|
|
211
|
+
]);
|
|
212
|
+
const clamp = useCallback((x, y) => clampWindowPosition(windowRef.current, x, y, live.current.size), [live, windowRef]);
|
|
213
|
+
const { hold } = useWindowLayout({
|
|
214
|
+
clamp,
|
|
215
|
+
commit,
|
|
216
|
+
dragging,
|
|
217
|
+
entry,
|
|
218
|
+
live,
|
|
219
|
+
resizing,
|
|
220
|
+
setPosition,
|
|
221
|
+
size,
|
|
222
|
+
windowRef,
|
|
223
|
+
});
|
|
224
|
+
// A finished gesture commits its geometry, then runs any layout it held
|
|
225
|
+
// off (a surface resize mid-gesture), even when the geometry did not change.
|
|
226
|
+
const finishGesture = useCallback(() => {
|
|
227
|
+
commit();
|
|
228
|
+
hold(false);
|
|
229
|
+
}, [commit, hold]);
|
|
230
|
+
useWindowPointerTracking(windowRef, dragging, resizing, clamp, setPosition, setSize, finishGesture);
|
|
231
|
+
const { handlePointerDown, handleResizePointerDown } = useGestureStarts(windowRef, maximized, position, dragging, resizing);
|
|
232
|
+
const stepped = useSteppedGeometry(windowRef, maximized, live, clamp, setPosition, setSize);
|
|
233
|
+
return {
|
|
234
|
+
...stepped,
|
|
235
|
+
commit,
|
|
236
|
+
hold,
|
|
237
|
+
handlePointerDown,
|
|
238
|
+
handleResizePointerDown,
|
|
239
|
+
position,
|
|
240
|
+
size,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=useWindowGeometry.js.map
|