@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
@@ -0,0 +1,45 @@
1
+ import { useCallback, useState } from "react";
2
+ /**
3
+ * Where to open the menu: the pointer, or — when the event carries no pointer
4
+ * position — the trigger's own box.
5
+ *
6
+ * Keyboard activation (Enter/Space on a kebab, the context-menu key on a row)
7
+ * dispatches with 0,0 client coordinates, which would pin the menu to the
8
+ * viewport's top-left corner, far from the control that opened it. Anchoring
9
+ * under the trigger instead matches what the standalone kebab menus already do
10
+ * (see MiniAppRowActionsButton's callers). A real click at exactly 0,0 lands in
11
+ * the very corner of the window, where none of these triggers can sit.
12
+ */
13
+ function getContextMenuPosition(event) {
14
+ if (event.clientX !== 0 || event.clientY !== 0) {
15
+ return { x: event.clientX, y: event.clientY };
16
+ }
17
+ const rect = event.currentTarget.getBoundingClientRect();
18
+ return { x: rect.left, y: rect.bottom };
19
+ }
20
+ export function useContextMenuState(params) {
21
+ const [contextMenu, setContextMenu] = useState(null);
22
+ const onOpen = params?.onOpen;
23
+ const openContextMenu = useCallback((event, id) => {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ onOpen?.(id);
27
+ setContextMenu({
28
+ id,
29
+ position: getContextMenuPosition(event),
30
+ });
31
+ }, [onOpen]);
32
+ const closeContextMenu = useCallback(() => setContextMenu(null), []);
33
+ return {
34
+ closeContextMenu,
35
+ contextMenu,
36
+ openContextMenu,
37
+ };
38
+ }
39
+ export function useContextMenuPositionState() {
40
+ const [contextMenu, setContextMenu] = useState(null);
41
+ const closeContextMenu = useCallback(() => setContextMenu(null), []);
42
+ const openContextMenuAt = useCallback((position) => setContextMenu(position), []);
43
+ return { closeContextMenu, contextMenu, openContextMenuAt };
44
+ }
45
+ //# sourceMappingURL=useContextMenuState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useContextMenuState.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/menu/useContextMenuState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAc/D;;;;;;;;;;GAUG;AACH,SAAS,sBAAsB,CAAC,KAA8B;IAC5D,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IAChD,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,aAAa,CAAC,qBAAqB,EAAE,CAAC;IACzD,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAe,MAEjD;IACC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAC5C,IAAI,CACL,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC;IAE9B,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,KAA8B,EAAE,EAAO,EAAE,EAAE;QAC1C,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,KAAK,CAAC,eAAe,EAAE,CAAC;QACxB,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACb,cAAc,CAAC;YACb,EAAE;YACF,QAAQ,EAAE,sBAAsB,CAAC,KAAK,CAAC;SACxC,CAAC,CAAC;IACL,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAErE,OAAO;QACL,gBAAgB;QAChB,WAAW;QACX,eAAe;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAC;IAC1E,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IACrE,MAAM,iBAAiB,GAAG,WAAW,CACnC,CAAC,QAAsB,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,EACpD,EAAE,CACH,CAAC;IAEF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC;AAC9D,CAAC","sourcesContent":["import { type MouseEvent, useCallback, useState } from \"react\";\nimport type { MenuPosition } from \"./Menu\";\n\nexport interface ContextMenuState<TId = string> {\n id: TId;\n position: MenuPosition;\n}\n\ninterface ContextMenuStateModel<TId = string> {\n closeContextMenu: () => void;\n contextMenu: ContextMenuState<TId> | null;\n openContextMenu: (event: MouseEvent<HTMLElement>, id: TId) => void;\n}\n\n/**\n * Where to open the menu: the pointer, or — when the event carries no pointer\n * position — the trigger's own box.\n *\n * Keyboard activation (Enter/Space on a kebab, the context-menu key on a row)\n * dispatches with 0,0 client coordinates, which would pin the menu to the\n * viewport's top-left corner, far from the control that opened it. Anchoring\n * under the trigger instead matches what the standalone kebab menus already do\n * (see MiniAppRowActionsButton's callers). A real click at exactly 0,0 lands in\n * the very corner of the window, where none of these triggers can sit.\n */\nfunction getContextMenuPosition(event: MouseEvent<HTMLElement>): MenuPosition {\n if (event.clientX !== 0 || event.clientY !== 0) {\n return { x: event.clientX, y: event.clientY };\n }\n\n const rect = event.currentTarget.getBoundingClientRect();\n return { x: rect.left, y: rect.bottom };\n}\n\nexport function useContextMenuState<TId = string>(params?: {\n onOpen?: (id: TId) => void;\n}): ContextMenuStateModel<TId> {\n const [contextMenu, setContextMenu] = useState<ContextMenuState<TId> | null>(\n null,\n );\n const onOpen = params?.onOpen;\n\n const openContextMenu = useCallback(\n (event: MouseEvent<HTMLElement>, id: TId) => {\n event.preventDefault();\n event.stopPropagation();\n onOpen?.(id);\n setContextMenu({\n id,\n position: getContextMenuPosition(event),\n });\n },\n [onOpen],\n );\n\n const closeContextMenu = useCallback(() => setContextMenu(null), []);\n\n return {\n closeContextMenu,\n contextMenu,\n openContextMenu,\n };\n}\n\nexport function useContextMenuPositionState() {\n const [contextMenu, setContextMenu] = useState<MenuPosition | null>(null);\n const closeContextMenu = useCallback(() => setContextMenu(null), []);\n const openContextMenuAt = useCallback(\n (position: MenuPosition) => setContextMenu(position),\n [],\n );\n\n return { closeContextMenu, contextMenu, openContextMenuAt };\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from "react";
2
+ export declare function useMenuKeyboard(menuRef: RefObject<HTMLDivElement | null>, ready: boolean, onClose: () => void): void;
@@ -0,0 +1,90 @@
1
+ import { useEffect, useEffectEvent } from "react";
2
+ function nextControl(controls, focused, key) {
3
+ if (controls.length === 0)
4
+ return undefined;
5
+ const index = focused instanceof HTMLElement ? controls.indexOf(focused) : -1;
6
+ switch (key) {
7
+ case "ArrowDown":
8
+ return controls[(index + 1) % controls.length];
9
+ case "ArrowUp":
10
+ return controls[index < 0
11
+ ? controls.length - 1
12
+ : (index - 1 + controls.length) % controls.length];
13
+ case "Home":
14
+ return controls[0];
15
+ case "End":
16
+ return controls.at(-1);
17
+ default:
18
+ return undefined;
19
+ }
20
+ }
21
+ export function useMenuKeyboard(menuRef, ready, onClose) {
22
+ const close = useEffectEvent(onClose);
23
+ useEffect(() => {
24
+ const menu = menuRef.current;
25
+ if (!ready || !menu)
26
+ return;
27
+ const trigger = document.activeElement;
28
+ let dismissedByPointer = false;
29
+ const items = () => Array.from(menu.querySelectorAll(":is(button, input[type=checkbox]):not(:disabled):not([hidden])"));
30
+ const focusFirst = () => (items()[0] ?? menu).focus({ preventScroll: true });
31
+ focusFirst();
32
+ // Startup and sync can replace an action while its menu is open. Recover
33
+ // focus only when that replacement dropped it onto the document body.
34
+ const observer = new MutationObserver(() => {
35
+ if (!dismissedByPointer && document.activeElement === document.body) {
36
+ focusFirst();
37
+ }
38
+ });
39
+ observer.observe(menu, { childList: true, subtree: true });
40
+ function restoreFocus() {
41
+ if (trigger instanceof HTMLElement && trigger.isConnected) {
42
+ trigger.focus({ preventScroll: true });
43
+ return document.activeElement === trigger && trigger !== document.body;
44
+ }
45
+ return false;
46
+ }
47
+ function handleKeyDown(event) {
48
+ if (event.altKey || event.ctrlKey || event.metaKey)
49
+ return;
50
+ if (!menu?.contains(document.activeElement))
51
+ return;
52
+ if (event.key === "Escape") {
53
+ event.preventDefault();
54
+ event.stopPropagation();
55
+ restoreFocus();
56
+ close();
57
+ return;
58
+ }
59
+ if (event.key === "Tab") {
60
+ if (!restoreFocus())
61
+ event.preventDefault();
62
+ close();
63
+ return;
64
+ }
65
+ const next = nextControl(items(), document.activeElement, event.key);
66
+ if (next) {
67
+ event.preventDefault();
68
+ event.stopPropagation();
69
+ next.focus();
70
+ }
71
+ }
72
+ function handleMouseDown(event) {
73
+ dismissedByPointer =
74
+ event.target instanceof Node && !menu?.contains(event.target);
75
+ }
76
+ document.addEventListener("mousedown", handleMouseDown, true);
77
+ document.addEventListener("keydown", handleKeyDown, true);
78
+ return () => {
79
+ observer.disconnect();
80
+ document.removeEventListener("mousedown", handleMouseDown, true);
81
+ document.removeEventListener("keydown", handleKeyDown, true);
82
+ if (!dismissedByPointer &&
83
+ (menu.contains(document.activeElement) ||
84
+ document.activeElement === document.body)) {
85
+ restoreFocus();
86
+ }
87
+ };
88
+ }, [menuRef, ready]);
89
+ }
90
+ //# sourceMappingURL=useMenuKeyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMenuKeyboard.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/menu/useMenuKeyboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { type RefObject, useEffect, useEffectEvent } from \"react\";\n\nfunction nextControl(\n controls: HTMLElement[],\n focused: Element | null,\n key: string,\n) {\n if (controls.length === 0) return undefined;\n const index = focused instanceof HTMLElement ? controls.indexOf(focused) : -1;\n switch (key) {\n case \"ArrowDown\":\n return controls[(index + 1) % controls.length];\n case \"ArrowUp\":\n return controls[\n index < 0\n ? controls.length - 1\n : (index - 1 + controls.length) % controls.length\n ];\n case \"Home\":\n return controls[0];\n case \"End\":\n return controls.at(-1);\n default:\n return undefined;\n }\n}\n\nexport function useMenuKeyboard(\n menuRef: RefObject<HTMLDivElement | null>,\n ready: boolean,\n onClose: () => void,\n) {\n const close = useEffectEvent(onClose);\n\n useEffect(() => {\n const menu = menuRef.current;\n if (!ready || !menu) return;\n\n const trigger = document.activeElement;\n let dismissedByPointer = false;\n const items = () =>\n Array.from(\n menu.querySelectorAll<HTMLElement>(\n \":is(button, input[type=checkbox]):not(:disabled):not([hidden])\",\n ),\n );\n const focusFirst = () =>\n (items()[0] ?? menu).focus({ preventScroll: true });\n focusFirst();\n // Startup and sync can replace an action while its menu is open. Recover\n // focus only when that replacement dropped it onto the document body.\n const observer = new MutationObserver(() => {\n if (!dismissedByPointer && document.activeElement === document.body) {\n focusFirst();\n }\n });\n observer.observe(menu, { childList: true, subtree: true });\n\n function restoreFocus() {\n if (trigger instanceof HTMLElement && trigger.isConnected) {\n trigger.focus({ preventScroll: true });\n return document.activeElement === trigger && trigger !== document.body;\n }\n return false;\n }\n\n function handleKeyDown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return;\n if (!menu?.contains(document.activeElement)) return;\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n restoreFocus();\n close();\n return;\n }\n if (event.key === \"Tab\") {\n if (!restoreFocus()) event.preventDefault();\n close();\n return;\n }\n const next = nextControl(items(), document.activeElement, event.key);\n if (next) {\n event.preventDefault();\n event.stopPropagation();\n next.focus();\n }\n }\n\n function handleMouseDown(event: MouseEvent) {\n dismissedByPointer =\n event.target instanceof Node && !menu?.contains(event.target);\n }\n\n document.addEventListener(\"mousedown\", handleMouseDown, true);\n document.addEventListener(\"keydown\", handleKeyDown, true);\n return () => {\n observer.disconnect();\n document.removeEventListener(\"mousedown\", handleMouseDown, true);\n document.removeEventListener(\"keydown\", handleKeyDown, true);\n if (\n !dismissedByPointer &&\n (menu.contains(document.activeElement) ||\n document.activeElement === document.body)\n ) {\n restoreFocus();\n }\n };\n }, [menuRef, ready]);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@tearleads/windowing",
3
+ "version": "0.1.1",
4
+ "description": "App-agnostic window management for React: window state, window chrome, and the menu and sidebar primitives it renders with.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "default": "./index.js"
13
+ },
14
+ "./*.css": "./*.css"
15
+ },
16
+ "sideEffects": [
17
+ "*.css",
18
+ "./index.js"
19
+ ],
20
+ "peerDependencies": {
21
+ "react": "^19.2.0",
22
+ "react-dom": "^19.2.0"
23
+ },
24
+ "dependencies": {
25
+ "@phosphor-icons/react": "^2.1.10"
26
+ }
27
+ }
@@ -0,0 +1,31 @@
1
+ .sidebar-resize-handle {
2
+ position: relative;
3
+ z-index: 1;
4
+ align-self: stretch;
5
+ flex-shrink: 0;
6
+ padding: 0;
7
+ border: 0;
8
+ cursor: col-resize;
9
+ background: transparent;
10
+ touch-action: none;
11
+ }
12
+
13
+ .sidebar-resize-handle::before {
14
+ position: absolute;
15
+ top: 0;
16
+ bottom: 0;
17
+ width: 1px;
18
+ background: var(--color-dark);
19
+ content: "";
20
+ opacity: var(--opacity-disabled);
21
+ }
22
+
23
+ .sidebar-resize-handle:hover::before,
24
+ .sidebar-resize-handle:focus-visible::before {
25
+ width: 2px;
26
+ opacity: var(--opacity-subtle);
27
+ }
28
+
29
+ .sidebar-resize-handle:focus-visible {
30
+ outline: none;
31
+ }
@@ -0,0 +1,18 @@
1
+ import { type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type RefObject } from "react";
2
+ export declare function useSidebarResize({ defaultWidth, initialWidth, }: {
3
+ defaultWidth: number;
4
+ initialWidth?: number | null;
5
+ }): {
6
+ handleKeyDown: (event: ReactKeyboardEvent<HTMLHRElement>) => void;
7
+ handlePointerDown: (event: ReactPointerEvent<HTMLHRElement>) => void;
8
+ resolvedWidth: number;
9
+ sidebarRef: RefObject<HTMLDivElement | null>;
10
+ width: number | null;
11
+ };
12
+ export declare function SidebarResizeHandle({ className, controls, currentWidth, onKeyDown, onPointerDown, }: {
13
+ className: string;
14
+ controls?: string | undefined;
15
+ currentWidth: number;
16
+ onKeyDown: (event: ReactKeyboardEvent<HTMLHRElement>) => void;
17
+ onPointerDown: (event: ReactPointerEvent<HTMLHRElement>) => void;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState, } from "react";
3
+ import "./SidebarResize.css";
4
+ const MIN_WIDTH = 80;
5
+ const MAX_WIDTH = 400;
6
+ const KEYBOARD_RESIZE_STEP = 10;
7
+ function clampSidebarWidth(width) {
8
+ return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width));
9
+ }
10
+ function useSidebarDragEffect({ active, draggingRef, endDrag, setWidth, }) {
11
+ useEffect(() => {
12
+ if (!active) {
13
+ return;
14
+ }
15
+ function handlePointerMove(event) {
16
+ const dragState = draggingRef.current;
17
+ if (!dragState || event.pointerId !== dragState.pointerId) {
18
+ return;
19
+ }
20
+ const dx = event.clientX - dragState.startX;
21
+ setWidth(clampSidebarWidth(dragState.startWidth + dx));
22
+ }
23
+ function handlePointerEnd(event) {
24
+ if (event.pointerId === draggingRef.current?.pointerId) {
25
+ endDrag();
26
+ }
27
+ }
28
+ document.addEventListener("pointermove", handlePointerMove);
29
+ document.addEventListener("pointerup", handlePointerEnd);
30
+ document.addEventListener("pointercancel", handlePointerEnd);
31
+ return () => {
32
+ document.removeEventListener("pointermove", handlePointerMove);
33
+ document.removeEventListener("pointerup", handlePointerEnd);
34
+ document.removeEventListener("pointercancel", handlePointerEnd);
35
+ endDrag();
36
+ };
37
+ }, [active, draggingRef, endDrag, setWidth]);
38
+ }
39
+ export function useSidebarResize({ defaultWidth, initialWidth = defaultWidth, }) {
40
+ const [width, setWidth] = useState(initialWidth);
41
+ const [isDragging, setIsDragging] = useState(false);
42
+ const sidebarRef = useRef(null);
43
+ const draggingRef = useRef(null);
44
+ const readCurrentWidth = useCallback(() => {
45
+ if (width !== null) {
46
+ return width;
47
+ }
48
+ const measuredWidth = sidebarRef.current?.getBoundingClientRect().width;
49
+ return measuredWidth && measuredWidth > 0
50
+ ? clampSidebarWidth(measuredWidth)
51
+ : defaultWidth;
52
+ }, [defaultWidth, width]);
53
+ const endDrag = useCallback(() => {
54
+ if (!draggingRef.current) {
55
+ return;
56
+ }
57
+ draggingRef.current = null;
58
+ setIsDragging(false);
59
+ document.body.style.cursor = "";
60
+ document.body.style.userSelect = "";
61
+ }, []);
62
+ useSidebarDragEffect({
63
+ active: isDragging,
64
+ draggingRef,
65
+ endDrag,
66
+ setWidth,
67
+ });
68
+ const handlePointerDown = useCallback((event) => {
69
+ if (event.button !== 0 || !event.isPrimary) {
70
+ return;
71
+ }
72
+ event.preventDefault();
73
+ const startWidth = readCurrentWidth();
74
+ draggingRef.current = {
75
+ pointerId: event.pointerId,
76
+ startWidth,
77
+ startX: event.clientX,
78
+ };
79
+ setWidth(startWidth);
80
+ setIsDragging(true);
81
+ document.body.style.cursor = "col-resize";
82
+ document.body.style.userSelect = "none";
83
+ }, [readCurrentWidth]);
84
+ const handleKeyDown = useCallback((event) => {
85
+ if (event.key === "Home") {
86
+ event.preventDefault();
87
+ setWidth(MIN_WIDTH);
88
+ return;
89
+ }
90
+ if (event.key === "End") {
91
+ event.preventDefault();
92
+ setWidth(MAX_WIDTH);
93
+ return;
94
+ }
95
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
96
+ return;
97
+ }
98
+ event.preventDefault();
99
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
100
+ const multiplier = event.shiftKey ? 5 : 1;
101
+ setWidth((currentWidth) => clampSidebarWidth((currentWidth ?? readCurrentWidth()) +
102
+ direction * KEYBOARD_RESIZE_STEP * multiplier));
103
+ }, [readCurrentWidth]);
104
+ return {
105
+ handleKeyDown,
106
+ handlePointerDown,
107
+ resolvedWidth: readCurrentWidth(),
108
+ sidebarRef,
109
+ width,
110
+ };
111
+ }
112
+ export function SidebarResizeHandle({ className, controls, currentWidth, onKeyDown, onPointerDown, }) {
113
+ return (_jsx("hr", { "aria-controls": controls, "aria-label": "Resize sidebar", "aria-orientation": "vertical", "aria-valuemax": MAX_WIDTH, "aria-valuemin": MIN_WIDTH, "aria-valuenow": currentWidth, className: `sidebar-resize-handle ${className}`, onKeyDown: onKeyDown, onPointerDown: onPointerDown, tabIndex: 0 }));
114
+ }
115
+ //# sourceMappingURL=SidebarResize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SidebarResize.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/sidebar/SidebarResize.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n type Dispatch,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type RefObject,\n type SetStateAction,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport \"./SidebarResize.css\";\n\nconst MIN_WIDTH = 80;\nconst MAX_WIDTH = 400;\nconst KEYBOARD_RESIZE_STEP = 10;\n\ninterface DragState {\n pointerId: number;\n startWidth: number;\n startX: number;\n}\n\nfunction clampSidebarWidth(width: number) {\n return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width));\n}\n\nfunction useSidebarDragEffect({\n active,\n draggingRef,\n endDrag,\n setWidth,\n}: {\n active: boolean;\n draggingRef: RefObject<DragState | null>;\n endDrag: () => void;\n setWidth: Dispatch<SetStateAction<number | null>>;\n}) {\n useEffect(() => {\n if (!active) {\n return;\n }\n\n function handlePointerMove(event: PointerEvent) {\n const dragState = draggingRef.current;\n if (!dragState || event.pointerId !== dragState.pointerId) {\n return;\n }\n\n const dx = event.clientX - dragState.startX;\n setWidth(clampSidebarWidth(dragState.startWidth + dx));\n }\n\n function handlePointerEnd(event: PointerEvent) {\n if (event.pointerId === draggingRef.current?.pointerId) {\n endDrag();\n }\n }\n\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handlePointerEnd);\n document.addEventListener(\"pointercancel\", handlePointerEnd);\n return () => {\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handlePointerEnd);\n document.removeEventListener(\"pointercancel\", handlePointerEnd);\n endDrag();\n };\n }, [active, draggingRef, endDrag, setWidth]);\n}\n\nexport function useSidebarResize({\n defaultWidth,\n initialWidth = defaultWidth,\n}: {\n defaultWidth: number;\n initialWidth?: number | null;\n}) {\n const [width, setWidth] = useState<number | null>(initialWidth);\n const [isDragging, setIsDragging] = useState(false);\n const sidebarRef = useRef<HTMLDivElement>(null);\n const draggingRef = useRef<DragState | null>(null);\n\n const readCurrentWidth = useCallback(() => {\n if (width !== null) {\n return width;\n }\n\n const measuredWidth = sidebarRef.current?.getBoundingClientRect().width;\n return measuredWidth && measuredWidth > 0\n ? clampSidebarWidth(measuredWidth)\n : defaultWidth;\n }, [defaultWidth, width]);\n\n const endDrag = useCallback(() => {\n if (!draggingRef.current) {\n return;\n }\n\n draggingRef.current = null;\n setIsDragging(false);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n }, []);\n\n useSidebarDragEffect({\n active: isDragging,\n draggingRef,\n endDrag,\n setWidth,\n });\n\n const handlePointerDown = useCallback(\n (event: ReactPointerEvent<HTMLHRElement>) => {\n if (event.button !== 0 || !event.isPrimary) {\n return;\n }\n\n event.preventDefault();\n const startWidth = readCurrentWidth();\n draggingRef.current = {\n pointerId: event.pointerId,\n startWidth,\n startX: event.clientX,\n };\n setWidth(startWidth);\n setIsDragging(true);\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n },\n [readCurrentWidth],\n );\n\n const handleKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLHRElement>) => {\n if (event.key === \"Home\") {\n event.preventDefault();\n setWidth(MIN_WIDTH);\n return;\n }\n\n if (event.key === \"End\") {\n event.preventDefault();\n setWidth(MAX_WIDTH);\n return;\n }\n\n if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") {\n return;\n }\n\n event.preventDefault();\n const direction = event.key === \"ArrowLeft\" ? -1 : 1;\n const multiplier = event.shiftKey ? 5 : 1;\n setWidth((currentWidth) =>\n clampSidebarWidth(\n (currentWidth ?? readCurrentWidth()) +\n direction * KEYBOARD_RESIZE_STEP * multiplier,\n ),\n );\n },\n [readCurrentWidth],\n );\n\n return {\n handleKeyDown,\n handlePointerDown,\n resolvedWidth: readCurrentWidth(),\n sidebarRef,\n width,\n };\n}\n\nexport function SidebarResizeHandle({\n className,\n controls,\n currentWidth,\n onKeyDown,\n onPointerDown,\n}: {\n className: string;\n controls?: string | undefined;\n currentWidth: number;\n onKeyDown: (event: ReactKeyboardEvent<HTMLHRElement>) => void;\n onPointerDown: (event: ReactPointerEvent<HTMLHRElement>) => void;\n}) {\n return (\n <hr\n aria-controls={controls}\n aria-label=\"Resize sidebar\"\n aria-orientation=\"vertical\"\n aria-valuemax={MAX_WIDTH}\n aria-valuemin={MIN_WIDTH}\n aria-valuenow={currentWidth}\n className={`sidebar-resize-handle ${className}`}\n onKeyDown={onKeyDown}\n onPointerDown={onPointerDown}\n tabIndex={0}\n />\n );\n}\n"]}
package/tokens.css ADDED
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Defaults for every design token the window chrome reads, at zero
3
+ * specificity: a host's own values (any :root rule, a theme, an app-scoped
4
+ * block) always win, and a page that defines none still gets a complete light
5
+ * window. The values match Tearleads' light theme.
6
+ */
7
+ :where(:root) {
8
+ --color-dark: #333;
9
+ --color-light: #fff;
10
+ --color-muted: #eee;
11
+ --color-danger: #b00020;
12
+ --color-hairline: color-mix(in srgb, var(--color-dark) 18%, transparent);
13
+ --emphasis-surface: var(--color-dark);
14
+ --emphasis-text: var(--color-light);
15
+
16
+ --border-width: 1px;
17
+ --border-strong-color: var(--color-dark);
18
+ --border-strong: var(--border-width) solid var(--border-strong-color);
19
+
20
+ --space-2xs: 0.125rem;
21
+ --space-xs: 0.25rem;
22
+ --space-sm: 0.5rem;
23
+ --space-md: 1rem;
24
+ --font-size-sm: 0.9rem;
25
+ --control-height: 2rem;
26
+ --control-icon-size: 1rem;
27
+ --menu-item-padding: var(--space-sm) var(--space-md);
28
+
29
+ --opacity-disabled: 0.5;
30
+ --opacity-icon: 0.6;
31
+ --opacity-subtle: 0.85;
32
+ --scrollbar-gutter: auto;
33
+ --transition: 0.18s ease;
34
+
35
+ --window-bar-line-height: 1.25em;
36
+ --window-bar-block-padding: var(--space-xs);
37
+ --window-bar-inline-padding: var(--space-sm);
38
+ --window-bar-height: calc(
39
+ var(--window-bar-line-height) +
40
+ (var(--window-bar-block-padding) * 2)
41
+ );
42
+ --window-bar-padding: var(--window-bar-block-padding)
43
+ var(--window-bar-inline-padding);
44
+ --tearleads-window-titlebar-background: var(--color-dark);
45
+ --tearleads-window-titlebar-text: var(--color-light);
46
+
47
+ --app-radius-control: 0.375rem;
48
+ --app-radius-surface: 0.75rem;
49
+ --app-hover-surface: color-mix(
50
+ in srgb,
51
+ var(--color-dark) 8%,
52
+ var(--color-light)
53
+ );
54
+ --app-shadow-surface: 0 0.5rem 2rem rgb(0 0 0 / 14%);
55
+ --app-shadow-window:
56
+ 0 1rem 3rem rgb(0 0 0 / 12%), 0 0.25rem 0.75rem rgb(0 0 0 / 8%);
57
+ }
58
+
59
+ @media (prefers-reduced-motion: reduce) {
60
+ :where(:root) {
61
+ --transition: 0s;
62
+ }
63
+ }
@@ -0,0 +1,27 @@
1
+ import { type PropsWithChildren } from "react";
2
+ interface CurrentWindowContextValue {
3
+ close: () => void;
4
+ id: string;
5
+ overlayHost: HTMLElement | null;
6
+ showStatusMessage: (message: string) => void;
7
+ /**
8
+ * Hide the window's toolbar row until the returned release is called. Refcounted,
9
+ * so overlapping suppressions restore the row only once the last one releases.
10
+ */
11
+ suppressToolbar: () => () => void;
12
+ }
13
+ export declare function CurrentWindowProvider({ children, close, id, overlayHost, showStatusMessage, suppressToolbar, }: PropsWithChildren<CurrentWindowContextValue>): import("react").JSX.Element;
14
+ export declare function useCurrentWindow(): CurrentWindowContextValue | null;
15
+ /**
16
+ * Drop the host window's toolbar row while `active`.
17
+ *
18
+ * A full-pane overlay (the image viewer) carries its own toolbar, and the
19
+ * window's row would otherwise stack directly above it — two toolbars, one of
20
+ * them driving chrome the overlay covers. Outside a window this is inert, since
21
+ * the routed shell's overlays cover the whole viewport already.
22
+ *
23
+ * Suppressing in a layout effect keeps the row from being painted alongside the
24
+ * overlay's own toolbar for a frame and then resizing the pane out from under it.
25
+ */
26
+ export declare function useSuppressWindowToolbar(active: boolean): void;
27
+ export {};
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useLayoutEffect, useMemo, } from "react";
3
+ const CurrentWindowContext = createContext(null);
4
+ export function CurrentWindowProvider({ children, close, id, overlayHost, showStatusMessage, suppressToolbar, }) {
5
+ const value = useMemo(() => ({ close, id, overlayHost, showStatusMessage, suppressToolbar }), [close, id, overlayHost, showStatusMessage, suppressToolbar]);
6
+ return (_jsx(CurrentWindowContext.Provider, { value: value, children: children }));
7
+ }
8
+ export function useCurrentWindow() {
9
+ return useContext(CurrentWindowContext);
10
+ }
11
+ /**
12
+ * Drop the host window's toolbar row while `active`.
13
+ *
14
+ * A full-pane overlay (the image viewer) carries its own toolbar, and the
15
+ * window's row would otherwise stack directly above it — two toolbars, one of
16
+ * them driving chrome the overlay covers. Outside a window this is inert, since
17
+ * the routed shell's overlays cover the whole viewport already.
18
+ *
19
+ * Suppressing in a layout effect keeps the row from being painted alongside the
20
+ * overlay's own toolbar for a frame and then resizing the pane out from under it.
21
+ */
22
+ export function useSuppressWindowToolbar(active) {
23
+ const suppressToolbar = useCurrentWindow()?.suppressToolbar;
24
+ useLayoutEffect(() => {
25
+ if (!active || !suppressToolbar) {
26
+ return;
27
+ }
28
+ return suppressToolbar();
29
+ }, [active, suppressToolbar]);
30
+ }
31
+ //# sourceMappingURL=CurrentWindowContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CurrentWindowContext.js","sourceRoot":"","sources":["../../../../../../../Users/a2f0/github/tearleads5/packages/windowing/src/window/CurrentWindowContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,aAAa,EAEb,UAAU,EACV,eAAe,EACf,OAAO,GACR,MAAM,OAAO,CAAC;AAcf,MAAM,oBAAoB,GAAG,aAAa,CACxC,IAAI,CACL,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,EACpC,QAAQ,EACR,KAAK,EACL,EAAE,EACF,WAAW,EACX,iBAAiB,EACjB,eAAe,GAC8B;IAC7C,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC,EACtE,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,CAAC,CAC7D,CAAC;IAEF,OAAO,CACL,KAAC,oBAAoB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACxC,QAAQ,GACqB,CACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,UAAU,CAAC,oBAAoB,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAe;IACtD,MAAM,eAAe,GAAG,gBAAgB,EAAE,EAAE,eAAe,CAAC;IAE5D,eAAe,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;YAChC,OAAO;QACT,CAAC;QACD,OAAO,eAAe,EAAE,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;AAChC,CAAC","sourcesContent":["import {\n createContext,\n type PropsWithChildren,\n useContext,\n useLayoutEffect,\n useMemo,\n} from \"react\";\n\ninterface CurrentWindowContextValue {\n close: () => void;\n id: string;\n overlayHost: HTMLElement | null;\n showStatusMessage: (message: string) => void;\n /**\n * Hide the window's toolbar row until the returned release is called. Refcounted,\n * so overlapping suppressions restore the row only once the last one releases.\n */\n suppressToolbar: () => () => void;\n}\n\nconst CurrentWindowContext = createContext<CurrentWindowContextValue | null>(\n null,\n);\n\nexport function CurrentWindowProvider({\n children,\n close,\n id,\n overlayHost,\n showStatusMessage,\n suppressToolbar,\n}: PropsWithChildren<CurrentWindowContextValue>) {\n const value = useMemo(\n () => ({ close, id, overlayHost, showStatusMessage, suppressToolbar }),\n [close, id, overlayHost, showStatusMessage, suppressToolbar],\n );\n\n return (\n <CurrentWindowContext.Provider value={value}>\n {children}\n </CurrentWindowContext.Provider>\n );\n}\n\nexport function useCurrentWindow() {\n return useContext(CurrentWindowContext);\n}\n\n/**\n * Drop the host window's toolbar row while `active`.\n *\n * A full-pane overlay (the image viewer) carries its own toolbar, and the\n * window's row would otherwise stack directly above it — two toolbars, one of\n * them driving chrome the overlay covers. Outside a window this is inert, since\n * the routed shell's overlays cover the whole viewport already.\n *\n * Suppressing in a layout effect keeps the row from being painted alongside the\n * overlay's own toolbar for a frame and then resizing the pane out from under it.\n */\nexport function useSuppressWindowToolbar(active: boolean) {\n const suppressToolbar = useCurrentWindow()?.suppressToolbar;\n\n useLayoutEffect(() => {\n if (!active || !suppressToolbar) {\n return;\n }\n return suppressToolbar();\n }, [active, suppressToolbar]);\n}\n"]}
@@ -0,0 +1,32 @@
1
+ /*
2
+ * The chrome's sizes assume border-box (a maximized window fills its surface at
3
+ * 100%, border included), and box-sizing does not inherit, so the window sets
4
+ * it on everything it renders. At zero specificity, any host rule wins.
5
+ */
6
+ :where(.window),
7
+ :where(.window) :where(*) {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ .window {
12
+ position: absolute;
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: 900px;
16
+ height: 600px;
17
+ max-width: 100%;
18
+ max-height: 100%;
19
+ min-width: 200px;
20
+ min-height: 100px;
21
+ background: var(--color-light);
22
+ border: var(--border-strong);
23
+ border-radius: var(--app-radius-surface);
24
+ box-shadow: var(--app-shadow-window);
25
+ }
26
+
27
+ .window--maximized {
28
+ min-width: unset;
29
+ min-height: unset;
30
+ border-radius: 0;
31
+ box-shadow: none;
32
+ }
@@ -0,0 +1,11 @@
1
+ import { type ComponentType, type PropsWithChildren } from "react";
2
+ import { type WindowEntry } from "./WindowStateProvider/index.js";
3
+ export type WindowContentBoundary = ComponentType<PropsWithChildren<{
4
+ entry: WindowEntry;
5
+ }>>;
6
+ interface WindowProps {
7
+ ContentBoundary?: WindowContentBoundary | undefined;
8
+ windowId: string;
9
+ }
10
+ export declare function Window({ ContentBoundary, windowId }: WindowProps): import("react").JSX.Element | null;
11
+ export {};