@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
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @tearleads/windowing
|
|
2
|
+
|
|
3
|
+
App-agnostic window management for React: window state (open, focus order,
|
|
4
|
+
minimize/maximize, a per-window route and Back stack), window chrome (title
|
|
5
|
+
bar, menu bar, toolbar, sidebar, status bar, resize handles), and the menu and
|
|
6
|
+
sidebar primitives that chrome renders with.
|
|
7
|
+
|
|
8
|
+
The package knows nothing about the applications that run inside its windows.
|
|
9
|
+
It depends only on React, react-dom, and Phosphor icons.
|
|
10
|
+
|
|
11
|
+
## Composing a desktop
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
useWindowActions,
|
|
16
|
+
useWindowStateData,
|
|
17
|
+
Window,
|
|
18
|
+
WindowStateProvider,
|
|
19
|
+
} from "@tearleads/windowing";
|
|
20
|
+
|
|
21
|
+
function Desktop() {
|
|
22
|
+
const { windows } = useWindowStateData();
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ position: "relative", height: "100%" }}>
|
|
25
|
+
{windows.map((entry) => (
|
|
26
|
+
<Window key={entry.id} windowId={entry.id} />
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function OpenNotes() {
|
|
33
|
+
const { create } = useWindowActions();
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={() => create("Notes", 200, 160, NotesApp, { appId: "notes" })}
|
|
38
|
+
>
|
|
39
|
+
Notes
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
<WindowStateProvider>
|
|
45
|
+
<OpenNotes />
|
|
46
|
+
<Desktop />
|
|
47
|
+
</WindowStateProvider>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Windows are absolutely positioned and clamp to their parent element, so the
|
|
51
|
+
desktop surface must be a positioned container.
|
|
52
|
+
|
|
53
|
+
## Geometry and input
|
|
54
|
+
|
|
55
|
+
Windows move by their title bar and resize by their corners with pointer
|
|
56
|
+
events, so mouse, touch, and pen all work. While a gesture runs the geometry
|
|
57
|
+
stays local to that window; when it ends, the window commits its `position` and
|
|
58
|
+
`size` (surface-relative) to its `WindowEntry`. The first layout commits too,
|
|
59
|
+
so `position` always reflects where the window sits. Without `size` a window
|
|
60
|
+
takes its stylesheet's default size.
|
|
61
|
+
|
|
62
|
+
To save a layout, read `position` and `size` from `useWindowStateData()`. To
|
|
63
|
+
restore one, pass them back as `create` options, which take precedence over
|
|
64
|
+
`create`'s viewport `x`/`y`. `setGeometry(id, { position, size })` moves or
|
|
65
|
+
resizes a window programmatically.
|
|
66
|
+
|
|
67
|
+
## Slots
|
|
68
|
+
|
|
69
|
+
- **`ContentBoundary`** — `Window` renders a window's component inside this
|
|
70
|
+
optional wrapper, which receives the `WindowEntry`. Hosts use it to give
|
|
71
|
+
content app-specific context (a route provider, an error boundary) without the
|
|
72
|
+
window layer knowing about apps. Tearleads fills it in
|
|
73
|
+
`packages/app/src/mini-apps/MiniAppWindow.tsx`.
|
|
74
|
+
- **`appId`** — an opaque string a host attaches when creating a window. The
|
|
75
|
+
window layer only stores and compares it; the host interprets it.
|
|
76
|
+
|
|
77
|
+
Windows register their own menu items, title-bar actions, a Back action, and a
|
|
78
|
+
sidebar through the `useWindow*` hooks, from inside the window's content.
|
|
79
|
+
|
|
80
|
+
## Styles
|
|
81
|
+
|
|
82
|
+
Each component imports its own stylesheet, so a bundler that handles CSS imports
|
|
83
|
+
picks them up. Stylesheets are also exported by path (for example
|
|
84
|
+
`@tearleads/windowing/window/WindowTitleBar.css`) for surfaces that reuse the
|
|
85
|
+
chrome's look without rendering a window.
|
|
86
|
+
|
|
87
|
+
The stylesheets read design tokens (colors, spacing, radii, shadows, and the
|
|
88
|
+
`--window-bar-*` and `--tearleads-window-titlebar-*` chrome tokens). The package
|
|
89
|
+
entry imports `tokens.css`, which defines every one of them under
|
|
90
|
+
`:where(:root)`, so a page that defines none still renders a complete light
|
|
91
|
+
window. That rule has zero specificity: any `:root` rule, theme, or scoped block
|
|
92
|
+
the host writes overrides it. Tearleads overrides them with its own themes in
|
|
93
|
+
`packages/ui/src/styles.css` and `packages/app/src/shell/layout/AppChrome.css`.
|
|
94
|
+
|
|
95
|
+
Windows and menus lay out `border-box`, the box model their sizes assume. Since
|
|
96
|
+
`box-sizing` does not inherit, a zero-specificity rule sets it on the window or
|
|
97
|
+
menu and everything inside, including window content; any host rule overrides
|
|
98
|
+
it.
|
|
99
|
+
|
|
100
|
+
## Publishing
|
|
101
|
+
|
|
102
|
+
Inside this workspace the package exports its TypeScript source. The npm
|
|
103
|
+
package is built separately, into `dist/`, with its own generated manifest:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
bun run --cwd packages/windowing package # build dist/
|
|
107
|
+
bun run --cwd packages/windowing package:smoke # pack, install, render, typecheck, and bundle outside the workspace
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The built package is ES modules with declarations and source maps, plus every
|
|
111
|
+
stylesheet. React and react-dom are peer dependencies from 19.2, the first
|
|
112
|
+
release with `useEffectEvent`; Phosphor icons is its one dependency. The
|
|
113
|
+
stylesheets and the entry module (which imports `tokens.css`) are declared side
|
|
114
|
+
effects, so bundlers that trust `sideEffects` keep the token defaults.
|
|
115
|
+
`src/publishedPackage.test.ts` checks the built manifest and that no module
|
|
116
|
+
reaches outside the package, so a new workspace import fails the package's tests
|
|
117
|
+
rather than the publish. The declarations drop the stylesheet imports tsc keeps
|
|
118
|
+
in them, which a consumer's typecheck could not resolve. The smoke script
|
|
119
|
+
installs the lowest React the peer range admits, typechecks against the
|
|
120
|
+
declarations with TypeScript's defaults, and bundles a named import with webpack.
|
|
121
|
+
|
|
122
|
+
Publish from the repository root using an account that owns the `@tearleads`
|
|
123
|
+
npm scope:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npm login --registry https://registry.npmjs.org
|
|
127
|
+
bun run publish:npm:dry-run
|
|
128
|
+
bun run publish:npm
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`scripts/publishNpmModules.sh` publishes every Tearleads module required for the
|
|
132
|
+
standalone windowing extraction. That set is only `@tearleads/windowing`: it has
|
|
133
|
+
no workspace dependencies, and its React peers and Phosphor dependency already
|
|
134
|
+
exist on npm. `scripts/publishWindowing.sh` is the package-specific command,
|
|
135
|
+
also available as `bun run --cwd packages/windowing publish:npm`.
|
|
136
|
+
|
|
137
|
+
Both commands rebuild into a fresh temporary directory, publish its generated
|
|
138
|
+
consumer manifest with public access to `https://registry.npmjs.org`, and remove
|
|
139
|
+
the directory on success or failure. A temporary `.npmrc` overrides any
|
|
140
|
+
scope-specific registry without changing your npm configuration. A dry run
|
|
141
|
+
builds and previews the same package without uploading. They accept `--dry-run`,
|
|
142
|
+
`--tag <tag>` (default `latest`), and `--otp <code>`; for example:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
bun run publish:npm --dry-run --tag next
|
|
146
|
+
bun run publish:npm --tag next --otp 123456
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Run `package:smoke` separately before a release to verify the packed package in
|
|
150
|
+
an external consumer. It needs the network to install the consumer dependencies.
|
|
151
|
+
|
|
152
|
+
`ship-pr` bumps changed workspace packages before review and merge; the generated
|
|
153
|
+
manifest copies `version` from `package.json`. For a release outside that flow,
|
|
154
|
+
bump the version yourself before publishing. The
|
|
155
|
+
manifest declares `"license": "UNLICENSED"` until a license is chosen.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Context } from "react";
|
|
2
|
+
export interface RequiredContext<T> {
|
|
3
|
+
/** The raw context, for rendering `context.Provider` in the owning provider. */
|
|
4
|
+
context: Context<T | null>;
|
|
5
|
+
/** Non-throwing twin for surfaces that may render outside the provider. */
|
|
6
|
+
useOptional: () => T | null;
|
|
7
|
+
/** Reads the context value, throwing `errorMessage` without a provider. */
|
|
8
|
+
useRequired: () => T;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A context whose value is mandatory for consumers. Callers export
|
|
12
|
+
* `useRequired`/`useOptional` under their own hook names, so every
|
|
13
|
+
* provider-guard hook shares this one accessor pair instead of restating the
|
|
14
|
+
* `useContext`-then-throw boilerplate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createRequiredContext<T>(errorMessage: string): RequiredContext<T>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A context whose value is mandatory for consumers. Callers export
|
|
4
|
+
* `useRequired`/`useOptional` under their own hook names, so every
|
|
5
|
+
* provider-guard hook shares this one accessor pair instead of restating the
|
|
6
|
+
* `useContext`-then-throw boilerplate.
|
|
7
|
+
*/
|
|
8
|
+
export function createRequiredContext(errorMessage) {
|
|
9
|
+
const context = createContext(null);
|
|
10
|
+
function useOptional() {
|
|
11
|
+
return useContext(context);
|
|
12
|
+
}
|
|
13
|
+
function useRequired() {
|
|
14
|
+
const value = useContext(context);
|
|
15
|
+
if (value === null) {
|
|
16
|
+
throw new Error(errorMessage);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return { context, useOptional, useRequired };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=createRequiredContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRequiredContext.js","sourceRoot":"","sources":["../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/createRequiredContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAWhE;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,YAAoB;IAEpB,MAAM,OAAO,GAAG,aAAa,CAAW,IAAI,CAAC,CAAC;IAE9C,SAAS,WAAW;QAClB,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,SAAS,WAAW;QAClB,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAC/C,CAAC","sourcesContent":["import { type Context, createContext, useContext } from \"react\";\n\nexport interface RequiredContext<T> {\n /** The raw context, for rendering `context.Provider` in the owning provider. */\n context: Context<T | null>;\n /** Non-throwing twin for surfaces that may render outside the provider. */\n useOptional: () => T | null;\n /** Reads the context value, throwing `errorMessage` without a provider. */\n useRequired: () => T;\n}\n\n/**\n * A context whose value is mandatory for consumers. Callers export\n * `useRequired`/`useOptional` under their own hook names, so every\n * provider-guard hook shares this one accessor pair instead of restating the\n * `useContext`-then-throw boilerplate.\n */\nexport function createRequiredContext<T>(\n errorMessage: string,\n): RequiredContext<T> {\n const context = createContext<T | null>(null);\n\n function useOptional(): T | null {\n return useContext(context);\n }\n\n function useRequired(): T {\n const value = useContext(context);\n if (value === null) {\n throw new Error(errorMessage);\n }\n return value;\n }\n\n return { context, useOptional, useRequired };\n}\n"]}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createRequiredContext, type RequiredContext, } from "./createRequiredContext.js";
|
|
2
|
+
export { Menu, type MenuPosition } from "./menu/Menu.js";
|
|
3
|
+
export { MenuItem, type MenuItemProps } from "./menu/MenuItem.js";
|
|
4
|
+
export { type ContextMenuState, useContextMenuPositionState, useContextMenuState, } from "./menu/useContextMenuState.js";
|
|
5
|
+
export { SidebarResizeHandle, useSidebarResize } from "./sidebar/SidebarResize.js";
|
|
6
|
+
export { CurrentWindowProvider, useCurrentWindow, useSuppressWindowToolbar, } from "./window/CurrentWindowContext.js";
|
|
7
|
+
export { Window, type WindowContentBoundary } from "./window/Window.js";
|
|
8
|
+
export { WindowTitleBarActionButtons } from "./window/WindowChromeActions.js";
|
|
9
|
+
export { WindowCloseButton } from "./window/WindowCloseButton.js";
|
|
10
|
+
export { useWindowBackAction, useWindowBackActionValue, useWindowFileMenuItem, useWindowRefreshMenuItem, useWindowRefreshMenuItemValue, useWindowTitleBarAction, useWindowTitleBarActions, useWindowToolbarReservation, useWindowViewMenuItem, WindowMenuProvider, } from "./window/WindowMenuContext.js";
|
|
11
|
+
export { hasWindowSidebar, useRegisteredWindowSidebar, useWindowSidebar, WindowSidebarProvider, } from "./window/WindowSidebarContext.js";
|
|
12
|
+
export { findTopWindow, useWindowActions, useWindowStateData, type WindowCreateOptions, type WindowEntry, type WindowGeometry, type WindowPosition, type WindowSize, type WindowStateActions, type WindowStateData, WindowStateProvider, } from "./window/WindowStateProvider/index.js";
|
|
13
|
+
export { WindowToolBar } from "./window/WindowToolBar.js";
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "./tokens.css";
|
|
2
|
+
// The public windowing API: window state, window chrome and its registration
|
|
3
|
+
// hooks, and the menu and sidebar primitives the chrome renders with.
|
|
4
|
+
export { createRequiredContext, } from "./createRequiredContext.js";
|
|
5
|
+
export { Menu } from "./menu/Menu.js";
|
|
6
|
+
export { MenuItem } from "./menu/MenuItem.js";
|
|
7
|
+
export { useContextMenuPositionState, useContextMenuState, } from "./menu/useContextMenuState.js";
|
|
8
|
+
export { SidebarResizeHandle, useSidebarResize } from "./sidebar/SidebarResize.js";
|
|
9
|
+
export { CurrentWindowProvider, useCurrentWindow, useSuppressWindowToolbar, } from "./window/CurrentWindowContext.js";
|
|
10
|
+
export { Window } from "./window/Window.js";
|
|
11
|
+
export { WindowTitleBarActionButtons } from "./window/WindowChromeActions.js";
|
|
12
|
+
export { WindowCloseButton } from "./window/WindowCloseButton.js";
|
|
13
|
+
export { useWindowBackAction, useWindowBackActionValue, useWindowFileMenuItem, useWindowRefreshMenuItem, useWindowRefreshMenuItemValue, useWindowTitleBarAction, useWindowTitleBarActions, useWindowToolbarReservation, useWindowViewMenuItem, WindowMenuProvider, } from "./window/WindowMenuContext.js";
|
|
14
|
+
export { hasWindowSidebar, useRegisteredWindowSidebar, useWindowSidebar, WindowSidebarProvider, } from "./window/WindowSidebarContext.js";
|
|
15
|
+
export { findTopWindow, useWindowActions, useWindowStateData, WindowStateProvider, } from "./window/WindowStateProvider/index.js";
|
|
16
|
+
export { WindowToolBar } from "./window/WindowToolBar.js";
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAEtB,6EAA6E;AAC7E,sEAAsE;AACtE,OAAO,EACL,qBAAqB,GAEtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,IAAI,EAAqB,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAsB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAEL,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,MAAM,EAA8B,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAQlB,mBAAmB,GACpB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["import \"./tokens.css\";\n\n// The public windowing API: window state, window chrome and its registration\n// hooks, and the menu and sidebar primitives the chrome renders with.\nexport {\n createRequiredContext,\n type RequiredContext,\n} from \"./createRequiredContext\";\nexport { Menu, type MenuPosition } from \"./menu/Menu\";\nexport { MenuItem, type MenuItemProps } from \"./menu/MenuItem\";\nexport {\n type ContextMenuState,\n useContextMenuPositionState,\n useContextMenuState,\n} from \"./menu/useContextMenuState\";\nexport { SidebarResizeHandle, useSidebarResize } from \"./sidebar/SidebarResize\";\nexport {\n CurrentWindowProvider,\n useCurrentWindow,\n useSuppressWindowToolbar,\n} from \"./window/CurrentWindowContext\";\nexport { Window, type WindowContentBoundary } from \"./window/Window\";\nexport { WindowTitleBarActionButtons } from \"./window/WindowChromeActions\";\nexport { WindowCloseButton } from \"./window/WindowCloseButton\";\nexport {\n useWindowBackAction,\n useWindowBackActionValue,\n useWindowFileMenuItem,\n useWindowRefreshMenuItem,\n useWindowRefreshMenuItemValue,\n useWindowTitleBarAction,\n useWindowTitleBarActions,\n useWindowToolbarReservation,\n useWindowViewMenuItem,\n WindowMenuProvider,\n} from \"./window/WindowMenuContext\";\nexport {\n hasWindowSidebar,\n useRegisteredWindowSidebar,\n useWindowSidebar,\n WindowSidebarProvider,\n} from \"./window/WindowSidebarContext\";\nexport {\n findTopWindow,\n useWindowActions,\n useWindowStateData,\n type WindowCreateOptions,\n type WindowEntry,\n type WindowGeometry,\n type WindowPosition,\n type WindowSize,\n type WindowStateActions,\n type WindowStateData,\n WindowStateProvider,\n} from \"./window/WindowStateProvider\";\nexport { WindowToolBar } from \"./window/WindowToolBar\";\n"]}
|
package/menu/Menu.css
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.menu {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1000;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
width: max-content;
|
|
7
|
+
overflow: auto;
|
|
8
|
+
/* Touch. The menu closes on any scroll outside itself, so a swipe on it must
|
|
9
|
+
never pan the page. A menu whose content fits has no scroll range, is not
|
|
10
|
+
in the scroll chain at all, and overscroll-behavior would not apply — so
|
|
11
|
+
consume the gesture outright. When the menu, or a scroll container inside
|
|
12
|
+
it, overflows (flagged by Menu.tsx) vertical panning is allowed instead;
|
|
13
|
+
contain keeps a swipe at the end of the menu's own range from chaining
|
|
14
|
+
outward, and an inner scroller contains its own. */
|
|
15
|
+
touch-action: none;
|
|
16
|
+
overscroll-behavior: contain;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
background: var(--color-light);
|
|
19
|
+
border: var(--border-strong);
|
|
20
|
+
padding: var(--space-xs);
|
|
21
|
+
border-radius: var(--app-radius-surface);
|
|
22
|
+
box-shadow: var(--app-shadow-surface);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Menu items size border-box, like the window chrome (see Window.css). */
|
|
26
|
+
:where(.menu) :where(*) {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.menu[data-scrollable] {
|
|
31
|
+
touch-action: pan-y;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.menu button {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: var(--space-sm);
|
|
38
|
+
min-height: var(--control-height);
|
|
39
|
+
padding: var(--menu-item-padding);
|
|
40
|
+
border: none;
|
|
41
|
+
background: none;
|
|
42
|
+
color: inherit;
|
|
43
|
+
text-align: left;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
border-radius: var(--app-radius-control);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.menu button:hover:not(:disabled),
|
|
51
|
+
.menu button:focus-visible:not(:disabled) {
|
|
52
|
+
background: var(--app-hover-surface);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.menu button:disabled {
|
|
56
|
+
opacity: var(--opacity-disabled);
|
|
57
|
+
cursor: default;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.menu-item-icon {
|
|
61
|
+
flex: 0 0 auto;
|
|
62
|
+
opacity: var(--opacity-icon);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.menu-item-label {
|
|
66
|
+
flex: 1 1 auto;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* Touch: this popover menu is portaled to <body>, but the routed hook lives on
|
|
74
|
+
* <html>, so its items still resolve a 44px tap height in the routed layout.
|
|
75
|
+
*
|
|
76
|
+
* The row height was the only thing this block used to set, which left a
|
|
77
|
+
* compliant-by-measure row holding a mark and a label that both read as too
|
|
78
|
+
* small on a phone:
|
|
79
|
+
*
|
|
80
|
+
* - The LABEL never saw the routed type ramp at all. The global reset gives
|
|
81
|
+
* buttons `font-family: inherit` but NOT `font-size`, and `.menu button`
|
|
82
|
+
* sets no size of its own, so every menu item rendered at the UA button
|
|
83
|
+
* default (~13.3px) no matter what `--font-size-sm` resolved to. Scoped to
|
|
84
|
+
* the routed tier deliberately: the windowed root carries the monospace
|
|
85
|
+
* font-size quirk (~13px), which makes `sm` ~11.7px there — SMALLER than the
|
|
86
|
+
* UA default this replaces — so applying it in both layouts would shrink the
|
|
87
|
+
* windowed menus rather than leave them alone.
|
|
88
|
+
* - The ICON is drawn from `size={16}`, a width/height attribute that author
|
|
89
|
+
* CSS outranks, so one rule re-sizes every menu icon — MenuItem's own and
|
|
90
|
+
* any icon a select-menu option supplies — to the same touch glyph size the
|
|
91
|
+
* toolbars use. The trigger's caret sits outside `.menu`, so it is untouched.
|
|
92
|
+
*
|
|
93
|
+
* One shared menu backs every context menu in the app (Explorer, Contacts,
|
|
94
|
+
* Notes, Org Manager, Identity Manager, the panes, tables, and the window title
|
|
95
|
+
* bar), so this block is the whole fix.
|
|
96
|
+
*/
|
|
97
|
+
:root[data-navigation-mode="routed"] .menu button {
|
|
98
|
+
min-height: var(--control-height);
|
|
99
|
+
font-size: var(--font-size-sm);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:root[data-navigation-mode="routed"] .menu button svg {
|
|
103
|
+
width: var(--control-icon-size);
|
|
104
|
+
height: var(--control-icon-size);
|
|
105
|
+
}
|
package/menu/Menu.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PropsWithChildren } from "react";
|
|
2
|
+
export interface MenuPosition {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function Menu({ position, onClose, direction, keyboardNavigation, children, }: PropsWithChildren<{
|
|
7
|
+
position: MenuPosition;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
direction?: "up" | "down";
|
|
10
|
+
keyboardNavigation?: boolean;
|
|
11
|
+
}>): import("react").ReactPortal;
|
package/menu/Menu.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState, } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import "./Menu.css";
|
|
5
|
+
import { useMenuKeyboard } from "./useMenuKeyboard.js";
|
|
6
|
+
const MENU_VIEWPORT_MARGIN_PX = 8;
|
|
7
|
+
const MEASUREMENT_MENU_STYLE = {
|
|
8
|
+
left: MENU_VIEWPORT_MARGIN_PX,
|
|
9
|
+
pointerEvents: "none",
|
|
10
|
+
top: MENU_VIEWPORT_MARGIN_PX,
|
|
11
|
+
visibility: "hidden",
|
|
12
|
+
};
|
|
13
|
+
// Fractional layout or zoom can report a spurious pixel of overflow; treat
|
|
14
|
+
// that as fitting, since a false positive hands touch panning to the page.
|
|
15
|
+
const SCROLL_OVERFLOW_TOLERANCE_PX = 1;
|
|
16
|
+
function overflows(element) {
|
|
17
|
+
return (element.scrollHeight > element.clientHeight + SCROLL_OVERFLOW_TOLERANCE_PX);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether touch should be allowed to pan inside the menu: its own box scrolls,
|
|
21
|
+
* or a descendant scroll container (a select menu's option list) does. The
|
|
22
|
+
* menu box is a scroll container by its stylesheet; descendants qualify by
|
|
23
|
+
* their computed overflow so a plain overflowing block does not count.
|
|
24
|
+
*/
|
|
25
|
+
function hasTouchScrollableContent(menu) {
|
|
26
|
+
if (overflows(menu)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
for (const element of Array.from(menu.querySelectorAll("*"))) {
|
|
30
|
+
if (!overflows(element)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const overflowY = getComputedStyle(element).overflowY;
|
|
34
|
+
if (overflowY === "auto" || overflowY === "scroll") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Dismiss the menu on a pointer press or a scroll outside it. The closer is
|
|
42
|
+
* read through an effect event: consumers commonly pass an inline one, and
|
|
43
|
+
* the document listeners should subscribe once, not on every render.
|
|
44
|
+
*/
|
|
45
|
+
function useMenuOutsideDismissal(menuRef, onClose) {
|
|
46
|
+
const close = useEffectEvent(onClose);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
function handleClick(e) {
|
|
49
|
+
const target = e.target;
|
|
50
|
+
if (!(target instanceof Node)) {
|
|
51
|
+
close();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (menuRef.current && !menuRef.current.contains(target)) {
|
|
55
|
+
close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
document.addEventListener("mousedown", handleClick);
|
|
59
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
60
|
+
}, [menuRef]);
|
|
61
|
+
// The menu is anchored once, from its trigger's position, so a scroll
|
|
62
|
+
// anywhere else would leave it floating where the trigger used to be. Close
|
|
63
|
+
// it instead. Scrolls inside the menu — its own overflow, or a list it
|
|
64
|
+
// hosts — are the menu working as intended. Capture phase, because scroll
|
|
65
|
+
// events do not bubble. The check covers this menu's own subtree only: a
|
|
66
|
+
// nested Menu is portaled to the body too, so scrolling one would close its
|
|
67
|
+
// parent. Nothing nests menus today; revisit here if that changes.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
function handleScroll(e) {
|
|
70
|
+
const target = e.target;
|
|
71
|
+
if (target instanceof Node && menuRef.current?.contains(target)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
close();
|
|
75
|
+
}
|
|
76
|
+
document.addEventListener("scroll", handleScroll, true);
|
|
77
|
+
return () => document.removeEventListener("scroll", handleScroll, true);
|
|
78
|
+
}, [menuRef]);
|
|
79
|
+
}
|
|
80
|
+
function clamp(value, min, max) {
|
|
81
|
+
return Math.min(Math.max(value, min), max);
|
|
82
|
+
}
|
|
83
|
+
function createInitialMenuPlacement(position, direction) {
|
|
84
|
+
return {
|
|
85
|
+
anchorDirection: direction,
|
|
86
|
+
anchorX: position.x,
|
|
87
|
+
anchorY: position.y,
|
|
88
|
+
left: position.x,
|
|
89
|
+
maxHeight: undefined,
|
|
90
|
+
maxWidth: undefined,
|
|
91
|
+
measured: false,
|
|
92
|
+
top: position.y,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function placeMenuInViewport(input) {
|
|
96
|
+
const availableWidth = Math.max(0, input.viewportWidth - MENU_VIEWPORT_MARGIN_PX * 2);
|
|
97
|
+
const availableHeight = Math.max(0, input.viewportHeight - MENU_VIEWPORT_MARGIN_PX * 2);
|
|
98
|
+
const boundedWidth = Math.min(input.width, availableWidth);
|
|
99
|
+
const boundedHeight = Math.min(input.height, availableHeight);
|
|
100
|
+
const maxLeft = Math.max(MENU_VIEWPORT_MARGIN_PX, input.viewportWidth - MENU_VIEWPORT_MARGIN_PX - boundedWidth);
|
|
101
|
+
const maxTop = Math.max(MENU_VIEWPORT_MARGIN_PX, input.viewportHeight - MENU_VIEWPORT_MARGIN_PX - boundedHeight);
|
|
102
|
+
const preferredTop = input.direction === "up"
|
|
103
|
+
? input.position.y - boundedHeight
|
|
104
|
+
: input.position.y;
|
|
105
|
+
return {
|
|
106
|
+
anchorDirection: input.direction,
|
|
107
|
+
anchorX: input.position.x,
|
|
108
|
+
anchorY: input.position.y,
|
|
109
|
+
left: clamp(input.position.x, MENU_VIEWPORT_MARGIN_PX, maxLeft),
|
|
110
|
+
maxHeight: availableHeight > 0 ? availableHeight : undefined,
|
|
111
|
+
maxWidth: availableWidth > 0 ? availableWidth : undefined,
|
|
112
|
+
measured: true,
|
|
113
|
+
top: clamp(preferredTop, MENU_VIEWPORT_MARGIN_PX, maxTop),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function Menu({ position, onClose, direction = "up", keyboardNavigation = true, children, }) {
|
|
117
|
+
const menuRef = useRef(null);
|
|
118
|
+
useMenuOutsideDismissal(menuRef, onClose);
|
|
119
|
+
const { x, y } = position;
|
|
120
|
+
const [placement, setPlacement] = useState(() => createInitialMenuPlacement(position, direction));
|
|
121
|
+
// Whether anything in the menu scrolls, so the stylesheet can let touch pan
|
|
122
|
+
// it while consuming swipes on a menu that fits (see Menu.css).
|
|
123
|
+
const [scrollable, setScrollable] = useState(false);
|
|
124
|
+
// Re-measure after every commit as well as on resize below: children can be
|
|
125
|
+
// swapped or filtered while the menu is open without its box changing size,
|
|
126
|
+
// which moves scrollHeight but not the ResizeObserver. setState bails out
|
|
127
|
+
// when the answer is unchanged.
|
|
128
|
+
useLayoutEffect(() => {
|
|
129
|
+
const menu = menuRef.current;
|
|
130
|
+
if (menu) {
|
|
131
|
+
setScrollable(hasTouchScrollableContent(menu));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
useLayoutEffect(() => {
|
|
135
|
+
const menu = menuRef.current;
|
|
136
|
+
if (!menu) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const updatePlacement = () => {
|
|
140
|
+
const rect = menu.getBoundingClientRect();
|
|
141
|
+
setPlacement(placeMenuInViewport({
|
|
142
|
+
direction,
|
|
143
|
+
height: rect.height,
|
|
144
|
+
position,
|
|
145
|
+
viewportHeight: window.innerHeight,
|
|
146
|
+
viewportWidth: window.innerWidth,
|
|
147
|
+
width: rect.width,
|
|
148
|
+
}));
|
|
149
|
+
setScrollable(hasTouchScrollableContent(menu));
|
|
150
|
+
};
|
|
151
|
+
updatePlacement();
|
|
152
|
+
const observer = new ResizeObserver(updatePlacement);
|
|
153
|
+
observer.observe(menu);
|
|
154
|
+
window.addEventListener("resize", updatePlacement);
|
|
155
|
+
return () => {
|
|
156
|
+
observer.disconnect();
|
|
157
|
+
window.removeEventListener("resize", updatePlacement);
|
|
158
|
+
};
|
|
159
|
+
}, [x, y, direction]);
|
|
160
|
+
const placementMatchesAnchor = placement.measured &&
|
|
161
|
+
placement.anchorX === position.x &&
|
|
162
|
+
placement.anchorY === position.y &&
|
|
163
|
+
placement.anchorDirection === direction;
|
|
164
|
+
useMenuKeyboard(menuRef, placementMatchesAnchor && keyboardNavigation, onClose);
|
|
165
|
+
const menuStyle = placementMatchesAnchor
|
|
166
|
+
? {
|
|
167
|
+
left: placement.left,
|
|
168
|
+
maxHeight: placement.maxHeight,
|
|
169
|
+
maxWidth: placement.maxWidth,
|
|
170
|
+
top: placement.top,
|
|
171
|
+
}
|
|
172
|
+
: MEASUREMENT_MENU_STYLE;
|
|
173
|
+
return createPortal(_jsx("div", { ref: menuRef, className: "menu", "data-scrollable": scrollable || undefined, style: menuStyle, tabIndex: -1, children: children }), document.body);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=Menu.js.map
|
package/menu/Menu.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Menu.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/menu/Menu.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n type CSSProperties,\n type PropsWithChildren,\n type RefObject,\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport \"./Menu.css\";\nimport { useMenuKeyboard } from \"./useMenuKeyboard\";\n\nconst MENU_VIEWPORT_MARGIN_PX = 8;\nconst MEASUREMENT_MENU_STYLE: CSSProperties = {\n left: MENU_VIEWPORT_MARGIN_PX,\n pointerEvents: \"none\",\n top: MENU_VIEWPORT_MARGIN_PX,\n visibility: \"hidden\",\n};\n\n// Fractional layout or zoom can report a spurious pixel of overflow; treat\n// that as fitting, since a false positive hands touch panning to the page.\nconst SCROLL_OVERFLOW_TOLERANCE_PX = 1;\n\nfunction overflows(element: HTMLElement): boolean {\n return (\n element.scrollHeight > element.clientHeight + SCROLL_OVERFLOW_TOLERANCE_PX\n );\n}\n\n/**\n * Whether touch should be allowed to pan inside the menu: its own box scrolls,\n * or a descendant scroll container (a select menu's option list) does. The\n * menu box is a scroll container by its stylesheet; descendants qualify by\n * their computed overflow so a plain overflowing block does not count.\n */\nfunction hasTouchScrollableContent(menu: HTMLElement): boolean {\n if (overflows(menu)) {\n return true;\n }\n\n for (const element of Array.from(menu.querySelectorAll<HTMLElement>(\"*\"))) {\n if (!overflows(element)) {\n continue;\n }\n const overflowY = getComputedStyle(element).overflowY;\n if (overflowY === \"auto\" || overflowY === \"scroll\") {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Dismiss the menu on a pointer press or a scroll outside it. The closer is\n * read through an effect event: consumers commonly pass an inline one, and\n * the document listeners should subscribe once, not on every render.\n */\nfunction useMenuOutsideDismissal(\n menuRef: RefObject<HTMLDivElement | null>,\n onClose: () => void,\n): void {\n const close = useEffectEvent(onClose);\n\n useEffect(() => {\n function handleClick(e: MouseEvent) {\n const target = e.target;\n if (!(target instanceof Node)) {\n close();\n return;\n }\n\n if (menuRef.current && !menuRef.current.contains(target)) {\n close();\n }\n }\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [menuRef]);\n\n // The menu is anchored once, from its trigger's position, so a scroll\n // anywhere else would leave it floating where the trigger used to be. Close\n // it instead. Scrolls inside the menu — its own overflow, or a list it\n // hosts — are the menu working as intended. Capture phase, because scroll\n // events do not bubble. The check covers this menu's own subtree only: a\n // nested Menu is portaled to the body too, so scrolling one would close its\n // parent. Nothing nests menus today; revisit here if that changes.\n useEffect(() => {\n function handleScroll(e: Event) {\n const target = e.target;\n if (target instanceof Node && menuRef.current?.contains(target)) {\n return;\n }\n close();\n }\n document.addEventListener(\"scroll\", handleScroll, true);\n return () => document.removeEventListener(\"scroll\", handleScroll, true);\n }, [menuRef]);\n}\n\nexport interface MenuPosition {\n x: number;\n y: number;\n}\n\ninterface MenuPlacement {\n anchorDirection: \"up\" | \"down\";\n anchorX: number;\n anchorY: number;\n left: number;\n maxHeight: number | undefined;\n maxWidth: number | undefined;\n measured: boolean;\n top: number;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction createInitialMenuPlacement(\n position: MenuPosition,\n direction: \"up\" | \"down\",\n): MenuPlacement {\n return {\n anchorDirection: direction,\n anchorX: position.x,\n anchorY: position.y,\n left: position.x,\n maxHeight: undefined,\n maxWidth: undefined,\n measured: false,\n top: position.y,\n };\n}\n\nfunction placeMenuInViewport(input: {\n direction: \"up\" | \"down\";\n height: number;\n position: MenuPosition;\n viewportHeight: number;\n viewportWidth: number;\n width: number;\n}): MenuPlacement {\n const availableWidth = Math.max(\n 0,\n input.viewportWidth - MENU_VIEWPORT_MARGIN_PX * 2,\n );\n const availableHeight = Math.max(\n 0,\n input.viewportHeight - MENU_VIEWPORT_MARGIN_PX * 2,\n );\n const boundedWidth = Math.min(input.width, availableWidth);\n const boundedHeight = Math.min(input.height, availableHeight);\n const maxLeft = Math.max(\n MENU_VIEWPORT_MARGIN_PX,\n input.viewportWidth - MENU_VIEWPORT_MARGIN_PX - boundedWidth,\n );\n const maxTop = Math.max(\n MENU_VIEWPORT_MARGIN_PX,\n input.viewportHeight - MENU_VIEWPORT_MARGIN_PX - boundedHeight,\n );\n const preferredTop =\n input.direction === \"up\"\n ? input.position.y - boundedHeight\n : input.position.y;\n\n return {\n anchorDirection: input.direction,\n anchorX: input.position.x,\n anchorY: input.position.y,\n left: clamp(input.position.x, MENU_VIEWPORT_MARGIN_PX, maxLeft),\n maxHeight: availableHeight > 0 ? availableHeight : undefined,\n maxWidth: availableWidth > 0 ? availableWidth : undefined,\n measured: true,\n top: clamp(preferredTop, MENU_VIEWPORT_MARGIN_PX, maxTop),\n };\n}\n\nexport function Menu({\n position,\n onClose,\n direction = \"up\",\n keyboardNavigation = true,\n children,\n}: PropsWithChildren<{\n position: MenuPosition;\n onClose: () => void;\n direction?: \"up\" | \"down\";\n keyboardNavigation?: boolean;\n}>) {\n const menuRef = useRef<HTMLDivElement>(null);\n useMenuOutsideDismissal(menuRef, onClose);\n const { x, y } = position;\n const [placement, setPlacement] = useState(() =>\n createInitialMenuPlacement(position, direction),\n );\n // Whether anything in the menu scrolls, so the stylesheet can let touch pan\n // it while consuming swipes on a menu that fits (see Menu.css).\n const [scrollable, setScrollable] = useState(false);\n\n // Re-measure after every commit as well as on resize below: children can be\n // swapped or filtered while the menu is open without its box changing size,\n // which moves scrollHeight but not the ResizeObserver. setState bails out\n // when the answer is unchanged.\n useLayoutEffect(() => {\n const menu = menuRef.current;\n if (menu) {\n setScrollable(hasTouchScrollableContent(menu));\n }\n });\n\n useLayoutEffect(() => {\n const menu = menuRef.current;\n if (!menu) {\n return;\n }\n\n const updatePlacement = () => {\n const rect = menu.getBoundingClientRect();\n setPlacement(\n placeMenuInViewport({\n direction,\n height: rect.height,\n position,\n viewportHeight: window.innerHeight,\n viewportWidth: window.innerWidth,\n width: rect.width,\n }),\n );\n setScrollable(hasTouchScrollableContent(menu));\n };\n updatePlacement();\n const observer = new ResizeObserver(updatePlacement);\n observer.observe(menu);\n window.addEventListener(\"resize\", updatePlacement);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"resize\", updatePlacement);\n };\n }, [x, y, direction]);\n\n const placementMatchesAnchor =\n placement.measured &&\n placement.anchorX === position.x &&\n placement.anchorY === position.y &&\n placement.anchorDirection === direction;\n useMenuKeyboard(\n menuRef,\n placementMatchesAnchor && keyboardNavigation,\n onClose,\n );\n const menuStyle: CSSProperties = placementMatchesAnchor\n ? {\n left: placement.left,\n maxHeight: placement.maxHeight,\n maxWidth: placement.maxWidth,\n top: placement.top,\n }\n : MEASUREMENT_MENU_STYLE;\n\n return createPortal(\n <div\n ref={menuRef}\n className=\"menu\"\n data-scrollable={scrollable || undefined}\n style={menuStyle}\n tabIndex={-1}\n >\n {children}\n </div>,\n document.body,\n );\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Icon } from "@phosphor-icons/react";
|
|
2
|
+
import type { MouseEventHandler } from "react";
|
|
3
|
+
export interface MenuItemProps {
|
|
4
|
+
icon?: Icon;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onClick: MouseEventHandler<HTMLButtonElement>;
|
|
8
|
+
}
|
|
9
|
+
export declare function MenuItem({ icon: IconComponent, label, disabled, onClick, }: MenuItemProps): import("react").JSX.Element;
|
package/menu/MenuItem.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function MenuItem({ icon: IconComponent, label, disabled, onClick, }) {
|
|
3
|
+
return (_jsxs("button", { type: "button", disabled: disabled, onClick: onClick, children: [IconComponent && (_jsx(IconComponent, { "aria-hidden": "true", className: "menu-item-icon", focusable: "false", size: 16, weight: "regular" })), _jsx("span", { className: "menu-item-label", children: label })] }));
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=MenuItem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuItem.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/menu/MenuItem.tsx"],"names":[],"mappings":";AAUA,MAAM,UAAU,QAAQ,CAAC,EACvB,IAAI,EAAE,aAAa,EACnB,KAAK,EACL,QAAQ,EACR,OAAO,GACO;IACd,OAAO,CACL,kBAAQ,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,aACvD,aAAa,IAAI,CAChB,KAAC,aAAa,mBACA,MAAM,EAClB,SAAS,EAAC,gBAAgB,EAC1B,SAAS,EAAC,OAAO,EACjB,IAAI,EAAE,EAAE,EACR,MAAM,EAAC,SAAS,GAChB,CACH,EACD,eAAM,SAAS,EAAC,iBAAiB,YAAE,KAAK,GAAQ,IACzC,CACV,CAAC;AACJ,CAAC","sourcesContent":["import type { Icon } from \"@phosphor-icons/react\";\nimport type { MouseEventHandler } from \"react\";\n\nexport interface MenuItemProps {\n icon?: Icon;\n label: string;\n disabled?: boolean;\n onClick: MouseEventHandler<HTMLButtonElement>;\n}\n\nexport function MenuItem({\n icon: IconComponent,\n label,\n disabled,\n onClick,\n}: MenuItemProps) {\n return (\n <button type=\"button\" disabled={disabled} onClick={onClick}>\n {IconComponent && (\n <IconComponent\n aria-hidden=\"true\"\n className=\"menu-item-icon\"\n focusable=\"false\"\n size={16}\n weight=\"regular\"\n />\n )}\n <span className=\"menu-item-label\">{label}</span>\n </button>\n );\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type MouseEvent } from "react";
|
|
2
|
+
import type { MenuPosition } from "./Menu.js";
|
|
3
|
+
export interface ContextMenuState<TId = string> {
|
|
4
|
+
id: TId;
|
|
5
|
+
position: MenuPosition;
|
|
6
|
+
}
|
|
7
|
+
interface ContextMenuStateModel<TId = string> {
|
|
8
|
+
closeContextMenu: () => void;
|
|
9
|
+
contextMenu: ContextMenuState<TId> | null;
|
|
10
|
+
openContextMenu: (event: MouseEvent<HTMLElement>, id: TId) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function useContextMenuState<TId = string>(params?: {
|
|
13
|
+
onOpen?: (id: TId) => void;
|
|
14
|
+
}): ContextMenuStateModel<TId>;
|
|
15
|
+
export declare function useContextMenuPositionState(): {
|
|
16
|
+
closeContextMenu: () => void;
|
|
17
|
+
contextMenu: MenuPosition | null;
|
|
18
|
+
openContextMenuAt: (position: MenuPosition) => void;
|
|
19
|
+
};
|
|
20
|
+
export {};
|