@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.
Files changed (148) hide show
  1. package/README.md +155 -0
  2. package/createRequiredContext.d.ts +16 -0
  3. package/createRequiredContext.js +22 -0
  4. package/createRequiredContext.js.map +1 -0
  5. package/index.d.ts +13 -0
  6. package/index.js +17 -0
  7. package/index.js.map +1 -0
  8. package/menu/Menu.css +105 -0
  9. package/menu/Menu.d.ts +11 -0
  10. package/menu/Menu.js +175 -0
  11. package/menu/Menu.js.map +1 -0
  12. package/menu/MenuItem.d.ts +9 -0
  13. package/menu/MenuItem.js +5 -0
  14. package/menu/MenuItem.js.map +1 -0
  15. package/menu/useContextMenuState.d.ts +20 -0
  16. package/menu/useContextMenuState.js +45 -0
  17. package/menu/useContextMenuState.js.map +1 -0
  18. package/menu/useMenuKeyboard.d.ts +2 -0
  19. package/menu/useMenuKeyboard.js +90 -0
  20. package/menu/useMenuKeyboard.js.map +1 -0
  21. package/package.json +27 -0
  22. package/sidebar/SidebarResize.css +31 -0
  23. package/sidebar/SidebarResize.d.ts +18 -0
  24. package/sidebar/SidebarResize.js +115 -0
  25. package/sidebar/SidebarResize.js.map +1 -0
  26. package/tokens.css +63 -0
  27. package/window/CurrentWindowContext.d.ts +27 -0
  28. package/window/CurrentWindowContext.js +31 -0
  29. package/window/CurrentWindowContext.js.map +1 -0
  30. package/window/Window.css +32 -0
  31. package/window/Window.d.ts +11 -0
  32. package/window/Window.js +206 -0
  33. package/window/Window.js.map +1 -0
  34. package/window/WindowBody.css +21 -0
  35. package/window/WindowBody.d.ts +8 -0
  36. package/window/WindowBody.js +10 -0
  37. package/window/WindowBody.js.map +1 -0
  38. package/window/WindowChromeActions.d.ts +55 -0
  39. package/window/WindowChromeActions.js +61 -0
  40. package/window/WindowChromeActions.js.map +1 -0
  41. package/window/WindowCloseButton.d.ts +6 -0
  42. package/window/WindowCloseButton.js +9 -0
  43. package/window/WindowCloseButton.js.map +1 -0
  44. package/window/WindowMaximizeButton.d.ts +3 -0
  45. package/window/WindowMaximizeButton.js +5 -0
  46. package/window/WindowMaximizeButton.js.map +1 -0
  47. package/window/WindowMenuBar.css +63 -0
  48. package/window/WindowMenuBar.d.ts +14 -0
  49. package/window/WindowMenuBar.js +37 -0
  50. package/window/WindowMenuBar.js.map +1 -0
  51. package/window/WindowMenuContext.d.ts +46 -0
  52. package/window/WindowMenuContext.js +208 -0
  53. package/window/WindowMenuContext.js.map +1 -0
  54. package/window/WindowMinimizeButton.d.ts +3 -0
  55. package/window/WindowMinimizeButton.js +5 -0
  56. package/window/WindowMinimizeButton.js.map +1 -0
  57. package/window/WindowResizeHandle.css +65 -0
  58. package/window/WindowResizeHandle.d.ts +5 -0
  59. package/window/WindowResizeHandle.js +6 -0
  60. package/window/WindowResizeHandle.js.map +1 -0
  61. package/window/WindowSidebar.css +37 -0
  62. package/window/WindowSidebar.d.ts +6 -0
  63. package/window/WindowSidebar.js +9 -0
  64. package/window/WindowSidebar.js.map +1 -0
  65. package/window/WindowSidebarContext.d.ts +14 -0
  66. package/window/WindowSidebarContext.js +43 -0
  67. package/window/WindowSidebarContext.js.map +1 -0
  68. package/window/WindowStateProvider/WindowStateProvider.d.ts +2 -0
  69. package/window/WindowStateProvider/WindowStateProvider.js +16 -0
  70. package/window/WindowStateProvider/WindowStateProvider.js.map +1 -0
  71. package/window/WindowStateProvider/context.d.ts +4 -0
  72. package/window/WindowStateProvider/context.js +7 -0
  73. package/window/WindowStateProvider/context.js.map +1 -0
  74. package/window/WindowStateProvider/index.d.ts +5 -0
  75. package/window/WindowStateProvider/index.js +5 -0
  76. package/window/WindowStateProvider/index.js.map +1 -0
  77. package/window/WindowStateProvider/types.d.ts +58 -0
  78. package/window/WindowStateProvider/types.js +2 -0
  79. package/window/WindowStateProvider/types.js.map +1 -0
  80. package/window/WindowStateProvider/useWindowActions.d.ts +1 -0
  81. package/window/WindowStateProvider/useWindowActions.js +3 -0
  82. package/window/WindowStateProvider/useWindowActions.js.map +1 -0
  83. package/window/WindowStateProvider/useWindowStateActions.d.ts +24 -0
  84. package/window/WindowStateProvider/useWindowStateActions.js +176 -0
  85. package/window/WindowStateProvider/useWindowStateActions.js.map +1 -0
  86. package/window/WindowStateProvider/useWindowStateData.d.ts +1 -0
  87. package/window/WindowStateProvider/useWindowStateData.js +3 -0
  88. package/window/WindowStateProvider/useWindowStateData.js.map +1 -0
  89. package/window/WindowStateProvider/util/bringWindowToFront.d.ts +2 -0
  90. package/window/WindowStateProvider/util/bringWindowToFront.js +21 -0
  91. package/window/WindowStateProvider/util/bringWindowToFront.js.map +1 -0
  92. package/window/WindowStateProvider/util/createWindowEntry.d.ts +3 -0
  93. package/window/WindowStateProvider/util/createWindowEntry.js +20 -0
  94. package/window/WindowStateProvider/util/createWindowEntry.js.map +1 -0
  95. package/window/WindowStateProvider/util/findAdjacentWindow.d.ts +2 -0
  96. package/window/WindowStateProvider/util/findAdjacentWindow.js +22 -0
  97. package/window/WindowStateProvider/util/findAdjacentWindow.js.map +1 -0
  98. package/window/WindowStateProvider/util/findTopWindow.d.ts +3 -0
  99. package/window/WindowStateProvider/util/findTopWindow.js +7 -0
  100. package/window/WindowStateProvider/util/findTopWindow.js.map +1 -0
  101. package/window/WindowStateProvider/util/getMaxWindowZIndex.d.ts +2 -0
  102. package/window/WindowStateProvider/util/getMaxWindowZIndex.js +4 -0
  103. package/window/WindowStateProvider/util/getMaxWindowZIndex.js.map +1 -0
  104. package/window/WindowStateProvider/util/index.d.ts +6 -0
  105. package/window/WindowStateProvider/util/index.js +7 -0
  106. package/window/WindowStateProvider/util/index.js.map +1 -0
  107. package/window/WindowStateProvider/util/swapWindowZIndexes.d.ts +2 -0
  108. package/window/WindowStateProvider/util/swapWindowZIndexes.js +21 -0
  109. package/window/WindowStateProvider/util/swapWindowZIndexes.js.map +1 -0
  110. package/window/WindowStateProvider/util/updateWindowFlag.d.ts +2 -0
  111. package/window/WindowStateProvider/util/updateWindowFlag.js +4 -0
  112. package/window/WindowStateProvider/util/updateWindowFlag.js.map +1 -0
  113. package/window/WindowStatusBar.css +40 -0
  114. package/window/WindowStatusBar.d.ts +4 -0
  115. package/window/WindowStatusBar.js +20 -0
  116. package/window/WindowStatusBar.js.map +1 -0
  117. package/window/WindowTitleBar.css +88 -0
  118. package/window/WindowTitleBar.d.ts +18 -0
  119. package/window/WindowTitleBar.js +36 -0
  120. package/window/WindowTitleBar.js.map +1 -0
  121. package/window/WindowToolBar.css +75 -0
  122. package/window/WindowToolBar.d.ts +38 -0
  123. package/window/WindowToolBar.js +68 -0
  124. package/window/WindowToolBar.js.map +1 -0
  125. package/window/useWindowGeometry.d.ts +16 -0
  126. package/window/useWindowGeometry.js +243 -0
  127. package/window/useWindowGeometry.js.map +1 -0
  128. package/window/useWindowItemRegistration.d.ts +10 -0
  129. package/window/useWindowItemRegistration.js +46 -0
  130. package/window/useWindowItemRegistration.js.map +1 -0
  131. package/window/useWindowItemRegistry.d.ts +12 -0
  132. package/window/useWindowItemRegistry.js +45 -0
  133. package/window/useWindowItemRegistry.js.map +1 -0
  134. package/window/useWindowKeyboardGeometry.d.ts +21 -0
  135. package/window/useWindowKeyboardGeometry.js +139 -0
  136. package/window/useWindowKeyboardGeometry.js.map +1 -0
  137. package/window/useWindowLayout.d.ts +18 -0
  138. package/window/useWindowLayout.js +115 -0
  139. package/window/useWindowLayout.js.map +1 -0
  140. package/window/useWindowToolbarReservationRegistry.d.ts +8 -0
  141. package/window/useWindowToolbarReservationRegistry.js +19 -0
  142. package/window/useWindowToolbarReservationRegistry.js.map +1 -0
  143. package/window/windowGeometry.d.ts +36 -0
  144. package/window/windowGeometry.js +54 -0
  145. package/window/windowGeometry.js.map +1 -0
  146. package/window/windowMenuRegistrations.d.ts +18 -0
  147. package/window/windowMenuRegistrations.js +34 -0
  148. 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
@@ -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;
@@ -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 {};