efficient-shell 0.0.0-stage → 4.126.2

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +753 -2
  3. package/dist/Browser-DBRSIQ4J.js +478 -0
  4. package/dist/Browser-DBRSIQ4J.js.map +1 -0
  5. package/dist/Calculator-HCKFTSJR.js +188 -0
  6. package/dist/Calculator-HCKFTSJR.js.map +1 -0
  7. package/dist/ConfirmDialog-LJXMB45A.js +3 -0
  8. package/dist/ConfirmDialog-LJXMB45A.js.map +1 -0
  9. package/dist/CurrencyConverter-BKKORN6K.js +232 -0
  10. package/dist/CurrencyConverter-BKKORN6K.js.map +1 -0
  11. package/dist/Documents-5MD5SKPB.js +437 -0
  12. package/dist/Documents-5MD5SKPB.js.map +1 -0
  13. package/dist/Files-KY35DPWB.js +801 -0
  14. package/dist/Files-KY35DPWB.js.map +1 -0
  15. package/dist/Notepad-2BO5VEUG.js +406 -0
  16. package/dist/Notepad-2BO5VEUG.js.map +1 -0
  17. package/dist/PdfViewer-LSP3YNFC.js +4 -0
  18. package/dist/PdfViewer-LSP3YNFC.js.map +1 -0
  19. package/dist/PomodoroTimer-SVJ62RXM.js +629 -0
  20. package/dist/PomodoroTimer-SVJ62RXM.js.map +1 -0
  21. package/dist/Preview-N4UYYXG6.js +2955 -0
  22. package/dist/Preview-N4UYYXG6.js.map +1 -0
  23. package/dist/Sidebar-J76PV4QF.js +290 -0
  24. package/dist/Sidebar-J76PV4QF.js.map +1 -0
  25. package/dist/Spreadsheet-5EWZGMXN.js +515 -0
  26. package/dist/Spreadsheet-5EWZGMXN.js.map +1 -0
  27. package/dist/Stock-CDUS4VCZ.js +154 -0
  28. package/dist/Stock-CDUS4VCZ.js.map +1 -0
  29. package/dist/Weather-EEDBWBPQ.js +427 -0
  30. package/dist/Weather-EEDBWBPQ.js.map +1 -0
  31. package/dist/WorldClock-FFFULVTJ.js +198 -0
  32. package/dist/WorldClock-FFFULVTJ.js.map +1 -0
  33. package/dist/apps/index.d.ts +267 -0
  34. package/dist/apps/index.js +147 -0
  35. package/dist/apps/index.js.map +1 -0
  36. package/dist/brand.css +2 -0
  37. package/dist/chunk-2VM2FRZJ.js +215 -0
  38. package/dist/chunk-2VM2FRZJ.js.map +1 -0
  39. package/dist/chunk-32W6VY2J.js +549 -0
  40. package/dist/chunk-32W6VY2J.js.map +1 -0
  41. package/dist/chunk-3CUSBZWG.js +59 -0
  42. package/dist/chunk-3CUSBZWG.js.map +1 -0
  43. package/dist/chunk-3L463T5H.js +26 -0
  44. package/dist/chunk-3L463T5H.js.map +1 -0
  45. package/dist/chunk-4TVV2BIU.js +13334 -0
  46. package/dist/chunk-4TVV2BIU.js.map +1 -0
  47. package/dist/chunk-6XQHH7D2.js +105 -0
  48. package/dist/chunk-6XQHH7D2.js.map +1 -0
  49. package/dist/chunk-7CCHEEYC.js +94 -0
  50. package/dist/chunk-7CCHEEYC.js.map +1 -0
  51. package/dist/chunk-7KIOEKHS.js +617 -0
  52. package/dist/chunk-7KIOEKHS.js.map +1 -0
  53. package/dist/chunk-AXWC3332.js +1103 -0
  54. package/dist/chunk-AXWC3332.js.map +1 -0
  55. package/dist/chunk-BFBWLCO6.js +16 -0
  56. package/dist/chunk-BFBWLCO6.js.map +1 -0
  57. package/dist/chunk-C3NELUTX.js +79 -0
  58. package/dist/chunk-C3NELUTX.js.map +1 -0
  59. package/dist/chunk-CT3HVZRZ.js +26 -0
  60. package/dist/chunk-CT3HVZRZ.js.map +1 -0
  61. package/dist/chunk-D7PYW2QS.js +265 -0
  62. package/dist/chunk-D7PYW2QS.js.map +1 -0
  63. package/dist/chunk-DMCFOWOJ.js +153 -0
  64. package/dist/chunk-DMCFOWOJ.js.map +1 -0
  65. package/dist/chunk-FQB24B42.js +416 -0
  66. package/dist/chunk-FQB24B42.js.map +1 -0
  67. package/dist/chunk-GA35NEND.js +56 -0
  68. package/dist/chunk-GA35NEND.js.map +1 -0
  69. package/dist/chunk-HNH7FOYB.js +69 -0
  70. package/dist/chunk-HNH7FOYB.js.map +1 -0
  71. package/dist/chunk-HYZ5QMVO.js +462 -0
  72. package/dist/chunk-HYZ5QMVO.js.map +1 -0
  73. package/dist/chunk-I464GYJJ.js +17 -0
  74. package/dist/chunk-I464GYJJ.js.map +1 -0
  75. package/dist/chunk-JFQGPY7W.js +819 -0
  76. package/dist/chunk-JFQGPY7W.js.map +1 -0
  77. package/dist/chunk-MGGY74RY.js +14 -0
  78. package/dist/chunk-MGGY74RY.js.map +1 -0
  79. package/dist/chunk-N3CG5HG5.js +144 -0
  80. package/dist/chunk-N3CG5HG5.js.map +1 -0
  81. package/dist/chunk-W6XTG23K.js +3708 -0
  82. package/dist/chunk-W6XTG23K.js.map +1 -0
  83. package/dist/chunk-YG45DAUT.js +96 -0
  84. package/dist/chunk-YG45DAUT.js.map +1 -0
  85. package/dist/chunk-YMGXZABY.js +7 -0
  86. package/dist/chunk-YMGXZABY.js.map +1 -0
  87. package/dist/chunk-YNGPTOL6.js +46 -0
  88. package/dist/chunk-YNGPTOL6.js.map +1 -0
  89. package/dist/chunk-YNHLQ24V.js +400 -0
  90. package/dist/chunk-YNHLQ24V.js.map +1 -0
  91. package/dist/chunk-YNKK7NE3.js +171 -0
  92. package/dist/chunk-YNKK7NE3.js.map +1 -0
  93. package/dist/chunk-YPO6RDMX.js +20 -0
  94. package/dist/chunk-YPO6RDMX.js.map +1 -0
  95. package/dist/file-intake/index.d.ts +87 -0
  96. package/dist/file-intake/index.js +3 -0
  97. package/dist/file-intake/index.js.map +1 -0
  98. package/dist/index-dQia6NpB.d.ts +5904 -0
  99. package/dist/index.d.ts +2495 -0
  100. package/dist/index.js +7460 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/markdown/index.d.ts +118 -0
  103. package/dist/markdown/index.js +184 -0
  104. package/dist/markdown/index.js.map +1 -0
  105. package/dist/markup/index.d.ts +235 -0
  106. package/dist/markup/index.js +135 -0
  107. package/dist/markup/index.js.map +1 -0
  108. package/dist/page-tabs/index.d.ts +154 -0
  109. package/dist/page-tabs/index.js +706 -0
  110. package/dist/page-tabs/index.js.map +1 -0
  111. package/dist/shell.css +105 -0
  112. package/dist/sounds-NT4DEZGD.js +3 -0
  113. package/dist/sounds-NT4DEZGD.js.map +1 -0
  114. package/dist/styles.css +22 -0
  115. package/dist/themes.css +249 -0
  116. package/dist/types-BOinJXUf.d.ts +118 -0
  117. package/dist/ui/index.d.ts +4 -0
  118. package/dist/ui/index.js +15 -0
  119. package/dist/ui/index.js.map +1 -0
  120. package/dist/ui.css +1518 -0
  121. package/package.json +178 -4
@@ -0,0 +1,3708 @@
1
+ import { useShellAuth, navIcons } from './chunk-GA35NEND.js';
2
+ import { isStaleChunkError, CMD_Z, CMD_SHIFT_Z, LoadingSpinner, Result, Button_default, useIsMobile, useShellPrefs } from './chunk-YNHLQ24V.js';
3
+ import { useShellStrings, PopupMenu, PopupMenuItem, PopupMenuDivider, PopupMenuLabel } from './chunk-7KIOEKHS.js';
4
+ import { Z_LAYERS, confirm, runEscapeInterceptors } from './chunk-HYZ5QMVO.js';
5
+ import { createContext, Component, useContext, useLayoutEffect, useRef, useEffect, useState, useCallback, useSyncExternalStore, useMemo, isValidElement, cloneElement, useReducer, useId, Suspense } from 'react';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+ import { createPortal } from 'react-dom';
8
+ import { XMarkIcon } from '@heroicons/react/24/outline';
9
+ import { useLocation } from 'react-router-dom';
10
+ import { useQueryClient, useQuery } from '@tanstack/react-query';
11
+
12
+ // src/windowRegistry/types.ts
13
+ function isPageEntry(entry) {
14
+ return "component" in entry;
15
+ }
16
+ function isEntityEntry(entry) {
17
+ return "endpoint" in entry;
18
+ }
19
+ var _registry = {};
20
+ function setShellWindowRegistry(registry) {
21
+ _registry = registry;
22
+ }
23
+ var WINDOW_REGISTRY = new Proxy({}, {
24
+ get(_t, prop) {
25
+ return _registry[prop];
26
+ },
27
+ has(_t, prop) {
28
+ return prop in _registry;
29
+ },
30
+ ownKeys() {
31
+ return Object.keys(_registry);
32
+ },
33
+ getOwnPropertyDescriptor(_t, prop) {
34
+ if (prop in _registry) {
35
+ return { configurable: true, enumerable: true, value: _registry[prop] };
36
+ }
37
+ return void 0;
38
+ }
39
+ });
40
+
41
+ // src/api/client.ts
42
+ var _apiClient = null;
43
+ function setShellApiClient(instance) {
44
+ _apiClient = instance;
45
+ }
46
+ function isShellApiClientConfigured() {
47
+ return _apiClient !== null;
48
+ }
49
+ var NOOP_METHODS = /* @__PURE__ */ new Set([
50
+ "get",
51
+ "post",
52
+ "put",
53
+ "patch",
54
+ "delete",
55
+ "head",
56
+ "options",
57
+ "request"
58
+ ]);
59
+ var apiClient = new Proxy({}, {
60
+ get(_t, prop) {
61
+ if (_apiClient) return _apiClient[prop];
62
+ if (typeof prop === "string" && NOOP_METHODS.has(prop)) {
63
+ return () => Promise.resolve({ data: null, status: 0, statusText: "", headers: {}, config: {} });
64
+ }
65
+ return void 0;
66
+ }
67
+ });
68
+ var client_default = apiClient;
69
+ function WindowCrashedFallback({ error, onReload }) {
70
+ const stale = isStaleChunkError(error);
71
+ return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-3 px-6 py-12 text-center", children: [
72
+ /* @__PURE__ */ jsx("svg", { className: "h-8 w-8 text-amber-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" }) }),
73
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
74
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700", children: stale ? "This window needs the latest version of the app" : "This window crashed" }),
75
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-400 max-w-sm break-words", children: stale ? "A new version was deployed while this tab was open. Reload the page to pick it up." : error.message || String(error) })
76
+ ] }),
77
+ /* @__PURE__ */ jsx(
78
+ "button",
79
+ {
80
+ type: "button",
81
+ onClick: stale ? () => window.location.reload() : onReload,
82
+ className: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 text-sm font-medium rounded-lg",
83
+ children: stale ? "Reload page" : "Reload window"
84
+ }
85
+ )
86
+ ] });
87
+ }
88
+ var WindowErrorBoundary = class extends Component {
89
+ state = { error: null };
90
+ static getDerivedStateFromError(error) {
91
+ return { error: error instanceof Error ? error : new Error(String(error)) };
92
+ }
93
+ componentDidCatch(error, info) {
94
+ console.error("[react-os-shell] window content crashed:", error, info.componentStack);
95
+ }
96
+ reset = () => this.setState({ error: null });
97
+ render() {
98
+ const { error } = this.state;
99
+ if (error) {
100
+ return this.props.fallback ? this.props.fallback(error, this.reset) : /* @__PURE__ */ jsx(WindowCrashedFallback, { error, onReload: this.reset });
101
+ }
102
+ return this.props.children;
103
+ }
104
+ };
105
+ var UndoContext = createContext(null);
106
+ var BTN = "text-xs text-gray-500 hover:text-gray-700 disabled:opacity-40 disabled:hover:text-gray-500 disabled:cursor-default";
107
+ function UndoControls({ className = "", auto = false }) {
108
+ const ctx = useContext(UndoContext);
109
+ const claimOwnMount = ctx?.claimOwnMount;
110
+ useLayoutEffect(() => {
111
+ if (auto || !claimOwnMount) return;
112
+ claimOwnMount(true);
113
+ return () => claimOwnMount(false);
114
+ }, [auto, claimOwnMount]);
115
+ if (!ctx || !ctx.enabled) return null;
116
+ const { undo, redo, canUndo, canRedo, undoLabel, redoLabel } = ctx;
117
+ return /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-2 ${className}`, "data-undo-controls": auto ? "shell" : "form", children: [
118
+ /* @__PURE__ */ jsx(
119
+ "button",
120
+ {
121
+ type: "button",
122
+ onClick: undo,
123
+ disabled: !canUndo,
124
+ title: canUndo ? `Undo ${undoLabel} (${CMD_Z})` : "Nothing to undo",
125
+ className: BTN,
126
+ children: "Undo"
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ onClick: redo,
134
+ disabled: !canRedo,
135
+ title: canRedo ? `Redo ${redoLabel} (${CMD_SHIFT_Z})` : "Nothing to redo",
136
+ className: BTN,
137
+ children: "Redo"
138
+ }
139
+ )
140
+ ] });
141
+ }
142
+
143
+ // src/shell/entityFetchPolicy.ts
144
+ var RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([
145
+ 408,
146
+ // Request Timeout
147
+ 429
148
+ // Too Many Requests
149
+ ]);
150
+ var ENTITY_FETCH_MAX_RETRIES = 3;
151
+ var ENTITY_REFETCH_INTERVAL_MS = 6e4;
152
+ function entityFetchStatus(error) {
153
+ const status = error?.response?.status;
154
+ return typeof status === "number" ? status : void 0;
155
+ }
156
+ function isPermanentClientError(error) {
157
+ const status = entityFetchStatus(error);
158
+ if (status == null) return false;
159
+ return status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
160
+ }
161
+ function shouldRetryEntityFetch(failureCount, error) {
162
+ return !isPermanentClientError(error) && failureCount < ENTITY_FETCH_MAX_RETRIES;
163
+ }
164
+ function entityRefetchInterval(error) {
165
+ return isPermanentClientError(error) ? false : ENTITY_REFETCH_INTERVAL_MS;
166
+ }
167
+ function entityDetailUrl(endpoint, entityId) {
168
+ return `${endpoint}${encodeURIComponent(entityId)}/`;
169
+ }
170
+ function LoadingSpinner2() {
171
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-8 text-sm text-gray-400", children: "Loading..." });
172
+ }
173
+ var FILL_CLASS = "flex-1 min-h-0";
174
+ function EntityWindowLoading() {
175
+ return /* @__PURE__ */ jsx(LoadingSpinner, { label: "Loading record\u2026", padding: "", className: FILL_CLASS });
176
+ }
177
+ function normaliseEntitySnapshot(snapshot) {
178
+ return snapshot ?? void 0;
179
+ }
180
+ function EntityWindowFallback({
181
+ isPending,
182
+ isFetching,
183
+ enabled = true,
184
+ error,
185
+ onRetry
186
+ }) {
187
+ if (isFetching || isPending && enabled) return /* @__PURE__ */ jsx(EntityWindowLoading, {});
188
+ if (error) {
189
+ const status = entityFetchStatus(error);
190
+ if (status === 401) {
191
+ return /* @__PURE__ */ jsx(
192
+ Result,
193
+ {
194
+ status: "warning",
195
+ title: "Sign in required",
196
+ subTitle: "Your session may have expired. Sign in again to open this record.",
197
+ className: FILL_CLASS
198
+ }
199
+ );
200
+ }
201
+ if (status === 403) {
202
+ return /* @__PURE__ */ jsx(Result, { status: "403", className: FILL_CLASS });
203
+ }
204
+ if (status === 404 || status === 410) {
205
+ return /* @__PURE__ */ jsx(Result, { status: "404", title: "Record not found", className: FILL_CLASS });
206
+ }
207
+ const canRetry = status == null || status === 408 || status === 429 || status >= 500;
208
+ return /* @__PURE__ */ jsx(
209
+ Result,
210
+ {
211
+ status: "500",
212
+ title: "Couldn't load record",
213
+ subTitle: canRetry ? "Check your connection and try again." : "The server rejected this request.",
214
+ extra: canRetry ? /* @__PURE__ */ jsx(Button_default, { variant: "secondary", size: "sm", onClick: onRetry, children: "Try again" }) : void 0,
215
+ className: FILL_CLASS
216
+ }
217
+ );
218
+ }
219
+ return /* @__PURE__ */ jsx(Result, { status: "404", title: "Record not found", className: FILL_CLASS });
220
+ }
221
+ function EntityWindowState({
222
+ entity,
223
+ children,
224
+ ...fallback
225
+ }) {
226
+ if (entity != null) return /* @__PURE__ */ jsx(Fragment, { children: children(entity) });
227
+ return /* @__PURE__ */ jsx(EntityWindowFallback, { ...fallback });
228
+ }
229
+
230
+ // src/shell/draftWindow.ts
231
+ var TIMESTAMPED_PLACEHOLDER = /^[a-z]+(?:-[a-z]+)*-\d{13}$/;
232
+ function isDraftEntityId(entityId) {
233
+ return typeof entityId === "string" && (entityId.startsWith("new-") || TIMESTAMPED_PLACEHOLDER.test(entityId));
234
+ }
235
+ function isDraftEntityWindow(item) {
236
+ if (isDraftEntityId(item.entityId)) return true;
237
+ const snapshot = item.entitySnapshot;
238
+ return !!snapshot && typeof snapshot === "object" && (!!snapshot._new || !!snapshot._duplicate);
239
+ }
240
+ var MinimizedContext = createContext({
241
+ openWindows: [],
242
+ openEntity: () => {
243
+ },
244
+ openPage: () => {
245
+ },
246
+ closeEntity: () => {
247
+ },
248
+ minimize: () => {
249
+ },
250
+ items: [],
251
+ restore: () => {
252
+ },
253
+ remove: () => {
254
+ },
255
+ restoreIfMinimized: () => false,
256
+ openItems: []
257
+ });
258
+ var WindowDirtyContext = createContext(null);
259
+ function useWindowDirty(dirty) {
260
+ const register = useContext(WindowDirtyContext);
261
+ const idRef = useRef(null);
262
+ if (idRef.current === null) idRef.current = /* @__PURE__ */ Symbol("window-dirty");
263
+ useLayoutEffect(() => {
264
+ if (!register) return;
265
+ return register(idRef.current, dirty);
266
+ }, [register, dirty]);
267
+ useEffect(() => {
268
+ if (!register || !dirty) return;
269
+ const holdUnload = (e) => {
270
+ e.preventDefault();
271
+ e.returnValue = "";
272
+ };
273
+ window.addEventListener("beforeunload", holdUnload);
274
+ return () => window.removeEventListener("beforeunload", holdUnload);
275
+ }, [register, dirty]);
276
+ }
277
+ function useWindowDirtyRegistry() {
278
+ const registrations = useRef(/* @__PURE__ */ new Map());
279
+ const [dirty, setDirty] = useState(false);
280
+ const registerDirty = useCallback((id, value) => {
281
+ const updateAggregate = () => {
282
+ const next = Array.from(registrations.current.values()).some(Boolean);
283
+ setDirty((current) => current === next ? current : next);
284
+ };
285
+ registrations.current.set(id, value);
286
+ updateAggregate();
287
+ return () => {
288
+ registrations.current.delete(id);
289
+ updateAggregate();
290
+ };
291
+ }, []);
292
+ return { dirty, registerDirty };
293
+ }
294
+ function useWindowManager() {
295
+ return useContext(MinimizedContext);
296
+ }
297
+ function shortcutSpecFor(item) {
298
+ if (item.type !== "page" && isDraftEntityWindow(item)) return null;
299
+ const entityType = item.type === "page" ? "page" : item.entityType || "";
300
+ const entityId = item.type === "page" ? item.route || "" : item.entityId || "";
301
+ if (!entityType || !entityId) return null;
302
+ return { entityType, entityId, label: item.label };
303
+ }
304
+ function PageWindow({ item, onClose, accentRgb }) {
305
+ const { dirty, registerDirty } = useWindowDirtyRegistry();
306
+ const raw = WINDOW_REGISTRY[item.route];
307
+ if (!raw || !isPageEntry(raw)) return null;
308
+ const entry = raw;
309
+ const Component2 = entry.component;
310
+ return /* @__PURE__ */ jsx(Modal, { open: true, onClose, icon: navIcons[item.route], title: entry.label, size: entry.size || "2xl", dirty, allowPinOnTop: entry.allowPinOnTop, initialPosition: entry.initialPosition, widget: entry.widget, compact: entry.compact, appStyle: entry.appStyle, flushBody: entry.flushBody, autoHeight: entry.autoHeight, autoMinHeight: entry.autoMinHeight, autoWidth: entry.autoWidth, dimensions: entry.dimensions, windowKey: item.id, openedFromKey: item.openedFrom, accentRgb, children: /* @__PURE__ */ jsx(WindowDirtyContext.Provider, { value: registerDirty, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx(LoadingSpinner2, {}) }), children: /* @__PURE__ */ jsx(Component2, {}) }) }) });
311
+ }
312
+ function DocFavStar({ entityType, entityId, label }) {
313
+ const shellStrings = useShellStrings();
314
+ const queryClient = useQueryClient();
315
+ const { data: profile } = useQuery({ queryKey: ["my-profile-sidebar"], enabled: isShellApiClientConfigured(), queryFn: () => client_default.get("/auth/me/").then((r) => r.data) });
316
+ const favDocs = (profile?.preferences || {}).favorite_documents || [];
317
+ const isFav = favDocs.some((d) => d.entityType === entityType && d.entityId === entityId);
318
+ const toggle = () => {
319
+ const next = isFav ? favDocs.filter((d) => !(d.entityType === entityType && d.entityId === entityId)) : [...favDocs, { entityType, entityId, label }];
320
+ client_default.patch("/auth/me/", { preferences: { favorite_documents: next } }).then(() => {
321
+ queryClient.invalidateQueries({ queryKey: ["my-profile-sidebar"] });
322
+ });
323
+ };
324
+ return /* @__PURE__ */ jsx(
325
+ "button",
326
+ {
327
+ onClick: toggle,
328
+ title: isFav ? shellStrings.taskbar.removeFromDesktop : shellStrings.taskbar.addToDesktop,
329
+ className: `shrink-0 transition-colors ${isFav ? "text-yellow-500 hover:text-yellow-600" : "text-gray-300 hover:text-yellow-400"}`,
330
+ children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: isFav ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" }) })
331
+ }
332
+ );
333
+ }
334
+ function RestoredRegistryModal({ item, onClose, onMinimize, accentRgb }) {
335
+ const raw = WINDOW_REGISTRY[item.entityType];
336
+ if (!raw || !isEntityEntry(raw)) return null;
337
+ const entry = raw;
338
+ const [editing, setEditing] = useState(false);
339
+ const { dirty, registerDirty } = useWindowDirtyRegistry();
340
+ const qkPrefix = entry.queryKey || entry.endpoint.replace(/^\/|\/$/g, "").split("/").pop() || item.entityType;
341
+ const isDuplicate = item.entitySnapshot?._duplicate;
342
+ const isDraft = isDraftEntityId(item.entityId);
343
+ const fetchesDetail = !entry.selfFetching && !isDuplicate && !isDraft && isShellApiClientConfigured();
344
+ const entityQuery = useQuery({
345
+ queryKey: [qkPrefix, item.entityId],
346
+ queryFn: () => client_default.get(entityDetailUrl(entry.endpoint, String(item.entityId))).then((r) => r.data),
347
+ initialData: normaliseEntitySnapshot(item.entitySnapshot),
348
+ initialDataUpdatedAt: 0,
349
+ // Treat snapshot as stale so query refetches immediately
350
+ enabled: fetchesDetail,
351
+ staleTime: 0,
352
+ refetchOnWindowFocus: true,
353
+ refetchOnMount: "always",
354
+ // Fallback polling; WebSocket provides instant updates. A window whose
355
+ // entity the server says is not there stops polling instead of asking
356
+ // every 60s forever — see entityFetchPolicy.
357
+ refetchInterval: (query) => entityRefetchInterval(query.state.error),
358
+ refetchIntervalInBackground: false,
359
+ retry: shouldRetryEntityFetch
360
+ });
361
+ const entity = entityQuery.data;
362
+ const refetch = entityQuery.refetch;
363
+ useEffect(() => {
364
+ if (entry.selfFetching || isDuplicate || isDraft) return;
365
+ const handler = () => {
366
+ refetch();
367
+ };
368
+ window.addEventListener("modal-reorder", handler);
369
+ return () => window.removeEventListener("modal-reorder", handler);
370
+ }, [refetch, entry.selfFetching, isDuplicate, isDraft]);
371
+ const handleClose = useCallback(() => {
372
+ if (editing && !dirty) {
373
+ setEditing(false);
374
+ } else {
375
+ onClose();
376
+ }
377
+ }, [editing, dirty, onClose]);
378
+ const titleBase = entry.selfFetching ? item.label : entity ? entry.title(entity, editing, setEditing) : item.label;
379
+ const entityIconKey = entry.icon;
380
+ const entityIcon = entityIconKey ? navIcons[entityIconKey] : null;
381
+ const titleContent = titleBase;
382
+ const footerContent = !entry.selfFetching && entity && entry.footer && !isDuplicate ? entry.footer(entity) : void 0;
383
+ if (entry.rendersOwnModal) {
384
+ if (entry.selfFetching) {
385
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(LoadingSpinner2, {}), children: entry.render(entity ?? item.entitySnapshot, onClose, item.entityId, editing, setEditing) });
386
+ }
387
+ if (entity != null) {
388
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(Modal, { open: true, onClose, title: item.label, size: entry.size || "2xl", children: /* @__PURE__ */ jsx(EntityWindowLoading, {}) }), children: entry.render(entity, onClose, item.entityId, editing, setEditing) });
389
+ }
390
+ return /* @__PURE__ */ jsx(Modal, { open: true, onClose, title: item.label, size: entry.size || "2xl", children: /* @__PURE__ */ jsx(
391
+ EntityWindowFallback,
392
+ {
393
+ isPending: entityQuery.isPending,
394
+ isFetching: entityQuery.isFetching,
395
+ enabled: fetchesDetail,
396
+ error: entityQuery.error,
397
+ onRetry: refetch
398
+ }
399
+ ) });
400
+ }
401
+ return /* @__PURE__ */ jsx(
402
+ Modal,
403
+ {
404
+ open: true,
405
+ onClose: handleClose,
406
+ onMinimize,
407
+ initialBox: item.savedBox,
408
+ icon: entityIcon,
409
+ title: titleContent,
410
+ footer: footerContent,
411
+ copyText: item.label,
412
+ windowKey: item.id,
413
+ openedFromKey: item.openedFrom,
414
+ size: entry.size || "2xl",
415
+ dirty,
416
+ editing: editing || isDraft || isDuplicate,
417
+ dimensions: entry.dimensions,
418
+ autoHeight: entry.autoHeight,
419
+ autoMinHeight: entry.autoMinHeight,
420
+ autoWidth: entry.autoWidth,
421
+ appStyle: entry.appStyle,
422
+ flushBody: entry.flushBody,
423
+ accentRgb,
424
+ children: /* @__PURE__ */ jsx(WindowDirtyContext.Provider, { value: registerDirty, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(EntityWindowLoading, {}), children: entry.selfFetching ? entry.render(null, onClose, item.entityId, editing, setEditing) : /* @__PURE__ */ jsx(
425
+ EntityWindowState,
426
+ {
427
+ entity,
428
+ isPending: entityQuery.isPending,
429
+ isFetching: entityQuery.isFetching,
430
+ enabled: fetchesDetail,
431
+ error: entityQuery.error,
432
+ onRetry: refetch,
433
+ children: (current) => entry.render(current, onClose, item.entityId, editing, setEditing)
434
+ }
435
+ ) }) })
436
+ }
437
+ );
438
+ }
439
+ function findPanelByWindowKey(key) {
440
+ return document.querySelector(`[data-modal-panel][data-window-key="${key}"]`);
441
+ }
442
+ function currentlyActiveWindowKey() {
443
+ const activeId = getActiveModalId();
444
+ if (!activeId) return void 0;
445
+ const panel = document.querySelector(`[data-modal-panel][data-modal-id="${activeId}"]`);
446
+ return panel?.getAttribute("data-window-key") ?? void 0;
447
+ }
448
+ var routeByWindowKey = /* @__PURE__ */ new Map();
449
+ function getActiveWindowRoute() {
450
+ const key = currentlyActiveWindowKey();
451
+ if (!key) return void 0;
452
+ return routeByWindowKey.get(key);
453
+ }
454
+ function findPanelByLabel(label) {
455
+ const panels = document.querySelectorAll("[data-modal-panel]");
456
+ for (const p of Array.from(panels)) {
457
+ const t = p.querySelector("[data-window-title]");
458
+ if (t?.textContent?.includes(label)) return p;
459
+ }
460
+ return null;
461
+ }
462
+ var PEEK_STYLE_ID = "rosh-peek-style";
463
+ var PEEK_Z = 249;
464
+ var PREVIEW_Z = 300;
465
+ function ensurePeekStyle() {
466
+ if (typeof document === "undefined" || document.getElementById(PEEK_STYLE_ID)) return;
467
+ const style = document.createElement("style");
468
+ style.id = PEEK_STYLE_ID;
469
+ style.textContent = `body.rosh-peeking [data-modal-panel]{transition:opacity .18s ease}body.rosh-peeking [data-modal-panel]:not([data-peek-focus]){opacity:.4}body.rosh-peeking [data-modal-panel][data-peek-focus]{opacity:1}body.rosh-peeking [data-modal-panel][data-peek-focus]:not([data-pinned-top]){z-index:${PEEK_Z}!important}html.rosh-reduce-transparency body.rosh-peeking [data-modal-panel]:not([data-peek-focus]){opacity:.92}@media (prefers-reduced-motion:reduce){body.rosh-peeking [data-modal-panel]{transition:none}}`;
470
+ document.head.appendChild(style);
471
+ }
472
+ function setPeekFocus(panel) {
473
+ if (typeof document === "undefined") return;
474
+ ensurePeekStyle();
475
+ document.querySelectorAll("[data-modal-panel][data-peek-focus]").forEach((el) => el.removeAttribute("data-peek-focus"));
476
+ if (panel) panel.setAttribute("data-peek-focus", "");
477
+ document.body.classList.toggle("rosh-peeking", !!panel);
478
+ }
479
+ function clearPeekFocus() {
480
+ if (typeof document === "undefined") return;
481
+ document.body.classList.remove("rosh-peeking");
482
+ document.querySelectorAll("[data-modal-panel][data-peek-focus]").forEach((el) => el.removeAttribute("data-peek-focus"));
483
+ }
484
+ function ThumbCard({ id, label, maxW, maxH, titleAbove = false, onClick, onClose }) {
485
+ const shellStrings = useShellStrings();
486
+ const previewRef = useRef(null);
487
+ const [size, setSize] = useState({ w: maxW, h: Math.round(maxH * 0.75) });
488
+ const [hasSnapshot, setHasSnapshot] = useState(false);
489
+ useLayoutEffect(() => {
490
+ const inner = previewRef.current;
491
+ if (!inner) return;
492
+ const target = findPanelByWindowKey(id) ?? findPanelByLabel(label);
493
+ if (!target) {
494
+ setHasSnapshot(false);
495
+ return;
496
+ }
497
+ const rect = target.getBoundingClientRect();
498
+ if (rect.width === 0 || rect.height === 0) {
499
+ setHasSnapshot(false);
500
+ return;
501
+ }
502
+ const ratio = Math.min(maxW / rect.width, maxH / rect.height);
503
+ const cardW = Math.max(80, Math.round(rect.width * ratio));
504
+ const cardH = Math.max(60, Math.round(rect.height * ratio));
505
+ setSize({ w: cardW, h: cardH });
506
+ setHasSnapshot(true);
507
+ const clone = target.cloneNode(true);
508
+ clone.style.position = "absolute";
509
+ clone.style.top = "0";
510
+ clone.style.left = "0";
511
+ clone.style.right = "auto";
512
+ clone.style.bottom = "auto";
513
+ clone.style.width = rect.width + "px";
514
+ clone.style.height = rect.height + "px";
515
+ clone.style.transform = `scale(${ratio})`;
516
+ clone.style.transformOrigin = "top left";
517
+ clone.style.pointerEvents = "none";
518
+ clone.style.animation = "none";
519
+ clone.style.zIndex = "auto";
520
+ clone.removeAttribute("data-modal-panel");
521
+ clone.removeAttribute("data-modal-id");
522
+ clone.removeAttribute("data-window-key");
523
+ clone.querySelectorAll('[role="dialog"], [data-portal]').forEach((el) => {
524
+ el.style.display = "none";
525
+ });
526
+ inner.innerHTML = "";
527
+ inner.appendChild(clone);
528
+ return () => {
529
+ inner.innerHTML = "";
530
+ };
531
+ }, [id, label, maxW, maxH]);
532
+ return /* @__PURE__ */ jsxs(
533
+ "div",
534
+ {
535
+ style: { width: size.w, height: size.h },
536
+ className: "relative rounded-md overflow-hidden bg-white/95 border border-gray-300 shadow-md cursor-pointer ring-2 ring-transparent group-hover:ring-blue-400 transition shrink-0",
537
+ onClick,
538
+ children: [
539
+ /* @__PURE__ */ jsx("div", { ref: previewRef, className: "absolute inset-0 z-0 overflow-hidden" }),
540
+ !hasSnapshot && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center text-gray-400 bg-gray-50", children: [
541
+ /* @__PURE__ */ jsx("svg", { className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75A2.25 2.25 0 016 4.5h12a2.25 2.25 0 012.25 2.25v10.5A2.25 2.25 0 0118 19.5H6a2.25 2.25 0 01-2.25-2.25V6.75z M3.75 9h16.5" }) }),
542
+ /* @__PURE__ */ jsx("span", { className: "mt-1 text-[10px] uppercase tracking-wide", children: "Hidden" })
543
+ ] }),
544
+ !titleAbove && /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 left-0 right-0 z-10 px-2 py-1 text-[10px] font-medium text-white bg-gradient-to-t from-black/80 to-transparent truncate pointer-events-none", children: label }),
545
+ onClose && /* @__PURE__ */ jsx(
546
+ "button",
547
+ {
548
+ onClick: (e) => {
549
+ e.stopPropagation();
550
+ onClose();
551
+ },
552
+ className: "absolute top-1 right-1 z-20 h-5 w-5 rounded-full bg-gray-900/90 ring-1 ring-white/80 shadow-sm hover:bg-red-500 text-white flex items-center justify-center",
553
+ title: shellStrings.taskbar.closeWindow,
554
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
555
+ }
556
+ )
557
+ ]
558
+ }
559
+ );
560
+ }
561
+ function TaskbarTabPreview({ items, anchorEl, onActivate, onClose, onMouseEnter, onMouseLeave }) {
562
+ const shellStrings = useShellStrings();
563
+ const MAX_W = 240;
564
+ const MAX_H = 160;
565
+ const isGroup = items.length > 1;
566
+ const popoverRef = useRef(null);
567
+ const [pos, setPos] = useState({ left: -9999, top: -9999, ready: false });
568
+ const [taskbarPos, setTaskbarPos] = useState(
569
+ () => typeof document !== "undefined" ? getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom" : "bottom"
570
+ );
571
+ useLayoutEffect(() => {
572
+ const el = popoverRef.current;
573
+ if (!el) return;
574
+ const measure = () => {
575
+ const tabRect = anchorEl.getBoundingClientRect();
576
+ const popRect = el.getBoundingClientRect();
577
+ const tbPos = getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom";
578
+ setTaskbarPos(tbPos);
579
+ let left = tabRect.left + tabRect.width / 2 - popRect.width / 2;
580
+ let top;
581
+ if (tbPos === "top") {
582
+ top = tabRect.bottom + 8;
583
+ } else if (tbPos === "left") {
584
+ left = tabRect.right + 8;
585
+ top = tabRect.top + tabRect.height / 2 - popRect.height / 2;
586
+ } else if (tbPos === "right") {
587
+ left = tabRect.left - popRect.width - 8;
588
+ top = tabRect.top + tabRect.height / 2 - popRect.height / 2;
589
+ } else {
590
+ top = tabRect.top - popRect.height - 8;
591
+ }
592
+ left = Math.max(8, Math.min(left, window.innerWidth - popRect.width - 8));
593
+ top = Math.max(8, Math.min(top, window.innerHeight - popRect.height - 8));
594
+ setPos({ left, top, ready: true });
595
+ };
596
+ measure();
597
+ const ro = new ResizeObserver(measure);
598
+ ro.observe(el);
599
+ return () => ro.disconnect();
600
+ }, [anchorEl, items.length]);
601
+ useEffect(() => clearPeekFocus, []);
602
+ const titleBelow = taskbarPos === "top";
603
+ const titleClass = `${titleBelow ? "mt-1" : "mb-1"} max-w-[240px] truncate text-[11px] font-medium text-gray-900 bg-white/80 px-2 py-0.5 rounded shadow-sm ring-2 ring-transparent transition group-hover:ring-blue-400`;
604
+ return createPortal(
605
+ /* @__PURE__ */ jsx(
606
+ "div",
607
+ {
608
+ ref: popoverRef,
609
+ "data-taskbar-preview": "",
610
+ style: {
611
+ position: "fixed",
612
+ left: pos.left,
613
+ top: pos.top,
614
+ zIndex: PREVIEW_Z,
615
+ maxWidth: "calc(100vw - 16px)",
616
+ opacity: pos.ready ? 1 : 0
617
+ },
618
+ className: isGroup ? "flex gap-2 flex-wrap" : "",
619
+ onMouseEnter,
620
+ onMouseLeave: () => {
621
+ clearPeekFocus();
622
+ onMouseLeave();
623
+ },
624
+ children: items.map((it) => {
625
+ const panel = findPanelByWindowKey(it.id) ?? findPanelByLabel(it.label);
626
+ const bearing = panel ? panelOffscreenBearing(panel) : null;
627
+ return /* @__PURE__ */ jsxs(
628
+ "div",
629
+ {
630
+ className: "group flex flex-col items-center",
631
+ onMouseEnter: () => setPeekFocus(panel),
632
+ children: [
633
+ !titleBelow && /* @__PURE__ */ jsx("span", { className: titleClass, children: it.label }),
634
+ /* @__PURE__ */ jsx(
635
+ ThumbCard,
636
+ {
637
+ id: it.id,
638
+ label: it.label,
639
+ maxW: MAX_W,
640
+ maxH: MAX_H,
641
+ titleAbove: true,
642
+ onClick: () => onActivate(it.id),
643
+ onClose: () => onClose(it.id)
644
+ }
645
+ ),
646
+ titleBelow && /* @__PURE__ */ jsx("span", { className: titleClass, children: it.label }),
647
+ bearing !== null && /* @__PURE__ */ jsxs(
648
+ "span",
649
+ {
650
+ title: shellStrings.taskbar.offscreenHint,
651
+ className: "mt-1 inline-flex items-center gap-1 rounded bg-amber-500 px-1.5 py-0.5 text-[10px] font-semibold text-white shadow-sm",
652
+ children: [
653
+ /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", style: { transform: `rotate(${bearing}deg)` }, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 12h14m0 0l-5-5m5 5l-5 5" }) }),
654
+ "Off screen"
655
+ ]
656
+ }
657
+ )
658
+ ]
659
+ },
660
+ it.id
661
+ );
662
+ })
663
+ }
664
+ ),
665
+ document.body
666
+ );
667
+ }
668
+ function taskbarMenuStyle(x, y) {
669
+ const pos = getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom";
670
+ return pos === "top" ? { left: Math.min(x, window.innerWidth - 220), top: y + 4 } : pos === "left" ? { left: x + 4, top: Math.min(y, window.innerHeight - 160) } : pos === "right" ? { right: window.innerWidth - x + 4, top: Math.min(y, window.innerHeight - 160) } : { left: Math.min(x, window.innerWidth - 220), bottom: window.innerHeight - y + 4 };
671
+ }
672
+ function TaskbarWindows({ openWindows, onRemove, onSplitView, onActivate, onActivateById }) {
673
+ const shellStrings = useShellStrings();
674
+ const [target, setTarget] = useState(null);
675
+ useEffect(() => {
676
+ const el = document.getElementById("taskbar-windows");
677
+ if (el) setTarget(el);
678
+ else {
679
+ const t = setInterval(() => {
680
+ const e = document.getElementById("taskbar-windows");
681
+ if (e) {
682
+ setTarget(e);
683
+ clearInterval(t);
684
+ }
685
+ }, 100);
686
+ return () => clearInterval(t);
687
+ }
688
+ }, []);
689
+ const activeModalId = useSyncExternalStore(subscribeActive, getActiveModalId);
690
+ const [, setGeomTick] = useState(0);
691
+ useEffect(() => {
692
+ const bump = () => setGeomTick((t) => t + 1);
693
+ window.addEventListener("modal-geometry", bump);
694
+ window.addEventListener("resize", bump);
695
+ return () => {
696
+ window.removeEventListener("modal-geometry", bump);
697
+ window.removeEventListener("resize", bump);
698
+ };
699
+ }, []);
700
+ const [, forceTick] = useState(0);
701
+ useEffect(() => {
702
+ const onTitle = () => forceTick((t) => t + 1);
703
+ window.addEventListener("window-title-update", onTitle);
704
+ return () => window.removeEventListener("window-title-update", onTitle);
705
+ }, []);
706
+ const liveTitle = (label) => {
707
+ const panel = findPanelByLabel(label);
708
+ const titleEl = panel?.querySelector("[data-window-title]");
709
+ return titleEl?.textContent?.trim() || label;
710
+ };
711
+ const [hoveredItems, setHoveredItems] = useState(null);
712
+ const [hoveredAnchor, setHoveredAnchor] = useState(null);
713
+ const [groupMenu, setGroupMenu] = useState(null);
714
+ const hoverTimerRef = useRef(null);
715
+ const handleEnter = (items, el) => {
716
+ if (groupMenu) return;
717
+ if (hoverTimerRef.current) {
718
+ clearTimeout(hoverTimerRef.current);
719
+ hoverTimerRef.current = null;
720
+ }
721
+ hoverTimerRef.current = setTimeout(() => {
722
+ setHoveredItems(items);
723
+ setHoveredAnchor(el);
724
+ }, 350);
725
+ };
726
+ const dismissPreview = () => {
727
+ if (hoverTimerRef.current) {
728
+ clearTimeout(hoverTimerRef.current);
729
+ hoverTimerRef.current = null;
730
+ }
731
+ setHoveredItems(null);
732
+ setHoveredAnchor(null);
733
+ };
734
+ const handleLeave = () => {
735
+ if (hoverTimerRef.current) {
736
+ clearTimeout(hoverTimerRef.current);
737
+ hoverTimerRef.current = null;
738
+ }
739
+ hoverTimerRef.current = setTimeout(() => {
740
+ setHoveredItems(null);
741
+ setHoveredAnchor(null);
742
+ }, 150);
743
+ };
744
+ const cancelLeave = () => {
745
+ if (hoverTimerRef.current) {
746
+ clearTimeout(hoverTimerRef.current);
747
+ hoverTimerRef.current = null;
748
+ }
749
+ };
750
+ const tabWindows = openWindows.filter((item) => !item.route || !WINDOW_REGISTRY[item.route]?.utility);
751
+ if (!target || tabWindows.length === 0) return null;
752
+ const groups = [];
753
+ const idx = /* @__PURE__ */ new Map();
754
+ for (const item of tabWindows) {
755
+ const entry = item.route ? WINDOW_REGISTRY[item.route] : void 0;
756
+ const grp = entry?.taskbarGroup;
757
+ const key = grp?.key ?? item.route ?? `entity:${item.id}`;
758
+ const i = idx.get(key);
759
+ if (i !== void 0) {
760
+ groups[i].items.push(item);
761
+ } else {
762
+ idx.set(key, groups.length);
763
+ const label = grp?.label ?? entry?.label ?? item.label;
764
+ groups.push({ key, route: grp?.icon ?? item.route, label, items: [item] });
765
+ }
766
+ }
767
+ return createPortal(
768
+ /* @__PURE__ */ jsxs(Fragment, { children: [
769
+ groups.map((group) => {
770
+ const icon = group.route ? navIcons[group.route] : null;
771
+ const primary = group.items[group.items.length - 1];
772
+ let isActive = false;
773
+ if (activeModalId) {
774
+ const panel = document.querySelector(`[data-modal-id="${activeModalId}"]`);
775
+ if (panel) {
776
+ const titleEl = panel.querySelector("[data-window-title]");
777
+ const titleText = titleEl?.textContent ?? "";
778
+ isActive = group.items.some((it) => titleText.includes(it.label));
779
+ }
780
+ }
781
+ const isGrouped = group.items.length > 1;
782
+ const offscreen = group.items.some((it) => {
783
+ const p = findPanelByWindowKey(it.id) ?? findPanelByLabel(it.label);
784
+ return !!p && !isPanelFullyVisible(p);
785
+ });
786
+ return /* @__PURE__ */ jsxs(
787
+ "button",
788
+ {
789
+ onClick: () => onActivateById(primary.id),
790
+ onMouseEnter: (e) => handleEnter(group.items, e.currentTarget),
791
+ onMouseLeave: handleLeave,
792
+ onDoubleClick: (e) => {
793
+ e.stopPropagation();
794
+ window.dispatchEvent(new CustomEvent("modal-center", { detail: { windowKey: primary.id, label: primary.label } }));
795
+ },
796
+ onContextMenu: (e) => {
797
+ e.preventDefault();
798
+ e.stopPropagation();
799
+ dismissPreview();
800
+ if (isGrouped) {
801
+ setGroupMenu({ x: e.clientX, y: e.clientY, items: group.items, label: group.label });
802
+ return;
803
+ }
804
+ window.dispatchEvent(new CustomEvent("modal-context-menu", { detail: { windowKey: primary.id, label: primary.label, x: e.clientX, y: e.clientY } }));
805
+ },
806
+ style: { width: "var(--window-tab-width, 200px)", fontSize: "var(--window-tab-font-size, 12px)" },
807
+ "data-tab-group": group.key,
808
+ className: `group relative flex items-center gap-1.5 rounded-lg px-3 py-2 font-medium transition-all min-w-0 shrink ${isActive ? "bg-blue-100/60 border border-blue-400/60 text-blue-700" : "bg-gray-50/40 border border-gray-200/40 text-gray-700 hover:bg-gray-200/40"}`,
809
+ children: [
810
+ icon && isValidElement(icon) ? cloneElement(icon, { className: `h-3.5 w-3.5 shrink-0 ${isActive ? "text-blue-600" : "text-gray-400"}` }) : /* @__PURE__ */ jsx("svg", { className: `h-3.5 w-3.5 shrink-0 ${isActive ? "text-blue-600" : "text-gray-400"}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" }) }),
811
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: isGrouped ? group.label : liveTitle(primary.label) }),
812
+ offscreen && /* @__PURE__ */ jsx("span", { title: shellStrings.taskbar.offscreenDot, className: "shrink-0 h-1.5 w-1.5 rounded-full bg-amber-500" }),
813
+ isGrouped && /* @__PURE__ */ jsx("span", { className: "shrink-0 px-1.5 py-0.5 rounded-full bg-blue-500/80 text-white text-[10px] font-bold leading-none", children: group.items.length }),
814
+ !isGrouped && /* @__PURE__ */ jsx("span", { role: "button", onClick: (e) => {
815
+ e.stopPropagation();
816
+ onRemove(primary.id);
817
+ }, className: "ml-auto text-gray-400 hover:text-red-500 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) })
818
+ ]
819
+ },
820
+ group.key
821
+ );
822
+ }),
823
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
824
+ tabWindows.length >= 2 && /* @__PURE__ */ jsxs(
825
+ "button",
826
+ {
827
+ onClick: onSplitView,
828
+ title: shellStrings.taskbar.expose,
829
+ className: "flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium text-blue-600 border border-blue-300 hover:bg-blue-50 transition-colors shrink-0",
830
+ children: [
831
+ /* @__PURE__ */ jsxs("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: [
832
+ /* @__PURE__ */ jsx("rect", { x: "3.5", y: "3.5", width: "7", height: "7", rx: "1" }),
833
+ /* @__PURE__ */ jsx("rect", { x: "13.5", y: "3.5", width: "7", height: "7", rx: "1" }),
834
+ /* @__PURE__ */ jsx("rect", { x: "3.5", y: "13.5", width: "7", height: "7", rx: "1" }),
835
+ /* @__PURE__ */ jsx("rect", { x: "13.5", y: "13.5", width: "7", height: "7", rx: "1" })
836
+ ] }),
837
+ "Expos\xE9"
838
+ ]
839
+ }
840
+ ),
841
+ groupMenu && /* @__PURE__ */ jsxs(PopupMenu, { portal: true, minWidth: 200, style: taskbarMenuStyle(groupMenu.x, groupMenu.y), onClose: () => setGroupMenu(null), children: [
842
+ /* @__PURE__ */ jsx(PopupMenuLabel, { children: groupMenu.label }),
843
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
844
+ setGroupMenu(null);
845
+ groupMenu.items.forEach((it) => minimizeWindowByKey(it.id));
846
+ }, children: [
847
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 12h-15" }) }),
848
+ shellStrings.taskbar.minimizeAllWindows
849
+ ] }),
850
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
851
+ setGroupMenu(null);
852
+ groupMenu.items.forEach((it) => onActivateById(it.id));
853
+ }, children: [
854
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 3.75h6.5v6.5h-6.5zM13.75 8.75h6.5v6.5h-6.5z" }) }),
855
+ shellStrings.taskbar.restoreAllWindows
856
+ ] }),
857
+ /* @__PURE__ */ jsx(PopupMenuDivider, {}),
858
+ /* @__PURE__ */ jsxs(PopupMenuItem, { danger: true, onClick: () => {
859
+ setGroupMenu(null);
860
+ groupMenu.items.forEach((it) => onRemove(it.id));
861
+ }, children: [
862
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }),
863
+ `${shellStrings.taskbar.closeAllWindows} (${groupMenu.items.length})`
864
+ ] })
865
+ ] }),
866
+ hoveredItems && hoveredAnchor && /* @__PURE__ */ jsx(
867
+ TaskbarTabPreview,
868
+ {
869
+ items: hoveredItems,
870
+ anchorEl: hoveredAnchor,
871
+ onActivate: (id) => {
872
+ onActivateById(id);
873
+ setHoveredItems(null);
874
+ setHoveredAnchor(null);
875
+ },
876
+ onClose: (id) => {
877
+ onRemove(id);
878
+ setHoveredItems(null);
879
+ setHoveredAnchor(null);
880
+ },
881
+ onMouseEnter: cancelLeave,
882
+ onMouseLeave: handleLeave
883
+ }
884
+ )
885
+ ] }),
886
+ target
887
+ );
888
+ }
889
+ var AUTH_PAGES = ["/login", "/forgot-password", "/reset-password", "/force-change-password"];
890
+ function WindowManagerProvider({ children, windowAccentForRoute }) {
891
+ const location = useLocation();
892
+ const isAuthPage = AUTH_PAGES.some((p) => location.pathname.startsWith(p));
893
+ const [openWindows, setOpenWindows] = useState([]);
894
+ const openWindowsRef = useRef(openWindows);
895
+ useLayoutEffect(() => {
896
+ openWindowsRef.current = openWindows;
897
+ }, [openWindows]);
898
+ useEffect(() => {
899
+ routeByWindowKey.clear();
900
+ for (const w of openWindows) {
901
+ if (w.route) routeByWindowKey.set(w.id, w.route);
902
+ }
903
+ }, [openWindows]);
904
+ const forceRemoveWindow = useCallback((id) => {
905
+ setOpenWindows((prev) => prev.filter((m) => m.id !== id));
906
+ }, []);
907
+ const requestClose = useCallback((id) => {
908
+ if (!findPanelByWindowKey(id)) {
909
+ forceRemoveWindow(id);
910
+ return;
911
+ }
912
+ requestModalClose(id);
913
+ }, [forceRemoveWindow]);
914
+ const activateAfterMount = (windowKey) => {
915
+ setTimeout(() => {
916
+ const panel = document.querySelector(`[data-modal-panel][data-window-key="${windowKey}"]`);
917
+ const mid = panel?.getAttribute("data-modal-id");
918
+ if (mid) activateModal(mid);
919
+ }, 50);
920
+ };
921
+ const openEntity = useCallback((entityType, entityId, snapshot, label, route) => {
922
+ if (!WINDOW_REGISTRY[entityType] || !isEntityEntry(WINDOW_REGISTRY[entityType])) return;
923
+ const id = `${entityType}:${entityId}`;
924
+ const openedFrom = currentlyActiveWindowKey();
925
+ setOpenWindows((prev) => {
926
+ const existing = prev.find((m) => m.entityType === entityType && String(m.entityId) === String(entityId));
927
+ if (existing) {
928
+ activateAfterMount(existing.id);
929
+ return prev;
930
+ }
931
+ activateAfterMount(id);
932
+ return [...prev, {
933
+ id,
934
+ type: "modal",
935
+ label: label || entityId,
936
+ route: route || window.location.pathname,
937
+ entityType,
938
+ entityId,
939
+ entitySnapshot: normaliseEntitySnapshot(snapshot),
940
+ openedFrom
941
+ }];
942
+ });
943
+ }, []);
944
+ const openPage = useCallback((path) => {
945
+ if (!WINDOW_REGISTRY[path] || !isPageEntry(WINDOW_REGISTRY[path])) return;
946
+ const entry = WINDOW_REGISTRY[path];
947
+ if (entry.widget) {
948
+ const existing = openWindowsRef.current.find((m) => m.type === "page" && m.route === path);
949
+ if (existing) {
950
+ requestClose(existing.id);
951
+ return;
952
+ }
953
+ }
954
+ const openedFrom = currentlyActiveWindowKey();
955
+ setOpenWindows((prev) => {
956
+ if (entry.multiInstance) {
957
+ const id2 = `page:${path}:${Math.random().toString(36).slice(2, 8)}`;
958
+ activateAfterMount(id2);
959
+ return [...prev, {
960
+ id: id2,
961
+ type: "page",
962
+ label: entry.label,
963
+ route: path,
964
+ openedFrom
965
+ }];
966
+ }
967
+ const existing = prev.find((m) => m.type === "page" && m.route === path);
968
+ if (existing) {
969
+ if (entry.widget) return prev;
970
+ activateAfterMount(existing.id);
971
+ return prev;
972
+ }
973
+ const id = `page:${path}`;
974
+ activateAfterMount(id);
975
+ return [...prev, {
976
+ id,
977
+ type: "page",
978
+ label: entry.label,
979
+ route: path,
980
+ openedFrom
981
+ }];
982
+ });
983
+ }, [requestClose]);
984
+ const minimize = useCallback(() => {
985
+ }, []);
986
+ const restore = useCallback(() => {
987
+ }, []);
988
+ const closeEntity = requestClose;
989
+ const remove = requestClose;
990
+ const restoreIfMinimized = () => false;
991
+ return /* @__PURE__ */ jsxs(MinimizedContext.Provider, { value: { openWindows, openEntity, openPage, closeEntity, minimize, items: [], openItems: openWindows, restore, remove, restoreIfMinimized }, children: [
992
+ children,
993
+ /* @__PURE__ */ jsx(
994
+ TaskbarWindows,
995
+ {
996
+ openWindows,
997
+ onRemove: requestClose,
998
+ onSplitView: triggerSplitView,
999
+ onActivate: (label) => {
1000
+ const panels = document.querySelectorAll("[data-modal-panel]");
1001
+ panels.forEach((p) => {
1002
+ const titleEl = p.querySelector("[data-window-title]");
1003
+ if (titleEl?.textContent?.includes(label)) {
1004
+ const mid = p.getAttribute("data-modal-id");
1005
+ if (mid) activateModal(mid);
1006
+ }
1007
+ });
1008
+ },
1009
+ onActivateById: (id) => {
1010
+ const panel = document.querySelector(`[data-modal-panel][data-window-key="${id}"]`);
1011
+ if (!panel) return;
1012
+ if (!isPanelFullyVisible(panel)) {
1013
+ revealWindow(id);
1014
+ return;
1015
+ }
1016
+ const mid = panel.getAttribute("data-modal-id");
1017
+ if (mid) activateModal(mid);
1018
+ }
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsx(ExposeBackdrop, {}),
1022
+ !isAuthPage && openWindows.map((item) => {
1023
+ const accentRgb = item.route ? windowAccentForRoute?.(item.route) : void 0;
1024
+ return /* @__PURE__ */ jsx(
1025
+ WindowErrorBoundary,
1026
+ {
1027
+ fallback: (error, reset) => /* @__PURE__ */ jsx(Modal, { open: true, onClose: () => forceRemoveWindow(item.id), title: item.label, size: "md", autoHeight: true, windowKey: item.id, children: /* @__PURE__ */ jsx(WindowCrashedFallback, { error, onReload: reset }) }),
1028
+ children: /* @__PURE__ */ jsx(WindowShortcutProvider, { spec: shortcutSpecFor(item), children: /* @__PURE__ */ jsx(UndoProvider, { windowId: item.id, children: item.type === "page" ? /* @__PURE__ */ jsx(PageWindow, { item, onClose: () => forceRemoveWindow(item.id), accentRgb }) : /* @__PURE__ */ jsx(
1029
+ RestoredRegistryModal,
1030
+ {
1031
+ item,
1032
+ onClose: () => forceRemoveWindow(item.id),
1033
+ onMinimize: () => {
1034
+ },
1035
+ accentRgb
1036
+ }
1037
+ ) }) })
1038
+ },
1039
+ item.id
1040
+ );
1041
+ })
1042
+ ] });
1043
+ }
1044
+
1045
+ // src/shell/mobileSwipeStore.ts
1046
+ var _parentKey = null;
1047
+ var subs = /* @__PURE__ */ new Set();
1048
+ function getSwipingParentKey() {
1049
+ return _parentKey;
1050
+ }
1051
+ function setSwipingParentKey(key) {
1052
+ if (_parentKey === key) return;
1053
+ _parentKey = key;
1054
+ for (const cb of subs) cb();
1055
+ }
1056
+ function subscribeSwipingParentKey(cb) {
1057
+ subs.add(cb);
1058
+ return () => {
1059
+ subs.delete(cb);
1060
+ };
1061
+ }
1062
+
1063
+ // src/shell/perfEvents.ts
1064
+ var empty = () => ({ menus: 0, submenus: 0, menuKey: null, moveMs: 0, resizeMs: 0 });
1065
+ var collecting = false;
1066
+ var counts = empty();
1067
+ var NOOP = () => {
1068
+ };
1069
+ function setPerfCollecting(on) {
1070
+ collecting = on;
1071
+ counts = empty();
1072
+ }
1073
+ function markMenuOpen(layer, key) {
1074
+ if (!collecting) return;
1075
+ if (layer === "submenu") counts.submenus++;
1076
+ else counts.menus++;
1077
+ counts.menuKey = key;
1078
+ }
1079
+ function beginWindowGesture(kind) {
1080
+ if (!collecting) return NOOP;
1081
+ const startedAt = performance.now();
1082
+ let ended = false;
1083
+ return () => {
1084
+ if (ended) return;
1085
+ ended = true;
1086
+ if (!collecting) return;
1087
+ const elapsed = performance.now() - startedAt;
1088
+ if (kind === "move") counts.moveMs += elapsed;
1089
+ else counts.resizeMs += elapsed;
1090
+ };
1091
+ }
1092
+ function drainPerfEvents() {
1093
+ const drained = counts;
1094
+ counts = empty();
1095
+ return drained;
1096
+ }
1097
+
1098
+ // src/shell/workArea.ts
1099
+ var SIDEBAR_STRIP_W = 280;
1100
+ function computeMaximizedBox() {
1101
+ const cs = getComputedStyle(document.documentElement);
1102
+ const taskbarH = parseInt(cs.getPropertyValue("--taskbar-height")) || 0;
1103
+ const taskbarW = parseInt(cs.getPropertyValue("--taskbar-width")) || 0;
1104
+ const tbPos = cs.getPropertyValue("--taskbar-position")?.trim() || "bottom";
1105
+ const sidebarW = parseInt(cs.getPropertyValue("--sidebar-width")) || 0;
1106
+ return {
1107
+ x: (tbPos === "left" ? taskbarW : 0) + sidebarW,
1108
+ y: tbPos === "top" ? taskbarH : 0,
1109
+ w: window.innerWidth - (tbPos === "left" || tbPos === "right" ? taskbarW : 0) - sidebarW,
1110
+ h: window.innerHeight - (tbPos === "top" || tbPos === "bottom" ? taskbarH : 0)
1111
+ };
1112
+ }
1113
+ function boxFillsWorkArea(box, workArea2, sidebarReserved) {
1114
+ const TOL = 4;
1115
+ const otherW = sidebarReserved ? workArea2.w + SIDEBAR_STRIP_W : workArea2.w - SIDEBAR_STRIP_W;
1116
+ const fillsWidth = Math.abs(box.w - workArea2.w) <= TOL || Math.abs(box.w - otherW) <= TOL;
1117
+ return Math.abs(box.h - workArea2.h) <= TOL && fillsWidth;
1118
+ }
1119
+ function scrollerIsFluid({ over, residual }, probeGain) {
1120
+ return residual <= 1 || over - residual >= probeGain - 2;
1121
+ }
1122
+ function widthToFit(curW, probeGain, scrollers) {
1123
+ let need = curW;
1124
+ for (const s of scrollers) {
1125
+ if (scrollerIsFluid(s, probeGain)) need = Math.max(need, curW + s.over);
1126
+ }
1127
+ return Math.ceil(need);
1128
+ }
1129
+ function growBoxToWidth(box, w, area, margin) {
1130
+ const nextW = Math.min(w, area.w - margin * 2);
1131
+ if (nextW <= box.w + 1) return box;
1132
+ const minX = area.x + margin;
1133
+ const maxX = area.x + area.w - margin - nextW;
1134
+ const x = Math.min(Math.max(box.x - (nextW - box.w) / 2, minX), maxX);
1135
+ return { ...box, x: Math.round(x), w: Math.round(nextW) };
1136
+ }
1137
+ function isSidebarStripReserved() {
1138
+ return (parseInt(getComputedStyle(document.documentElement).getPropertyValue("--sidebar-width")) || 0) > 0;
1139
+ }
1140
+ function readAlwaysMaximizedFlag() {
1141
+ if (typeof document === "undefined") return false;
1142
+ return getComputedStyle(document.documentElement).getPropertyValue("--layout-mode")?.trim() === "sidebar";
1143
+ }
1144
+ var ModalIdContext = createContext("");
1145
+ var WindowShortcutContext = createContext(null);
1146
+ function WindowShortcutProvider({ spec, children }) {
1147
+ return /* @__PURE__ */ jsx(WindowShortcutContext.Provider, { value: spec, children });
1148
+ }
1149
+ var TITLE_STRIP_TYPES = /* @__PURE__ */ new Set(["button", "input", "textarea", "select", "kbd", "svg"]);
1150
+ var MODAL_SUBMIT_SELECTOR = 'button[type="submit"], button[data-submit], button.btn-submit, button.bg-green-600, button.bg-blue-600';
1151
+ function extractTitleText(node) {
1152
+ if (node == null || typeof node === "boolean") return "";
1153
+ if (typeof node === "string") return node;
1154
+ if (typeof node === "number") return String(node);
1155
+ if (Array.isArray(node)) return node.map(extractTitleText).join("");
1156
+ if (isValidElement(node)) {
1157
+ const el = node;
1158
+ const tag = typeof el.type === "string" ? el.type : "";
1159
+ if (TITLE_STRIP_TYPES.has(tag)) return "";
1160
+ return extractTitleText(el.props?.children);
1161
+ }
1162
+ return "";
1163
+ }
1164
+ function useWidgetSettings(handler) {
1165
+ const id = useContext(ModalIdContext);
1166
+ useEffect(() => {
1167
+ if (!id) return;
1168
+ const listener = (e) => {
1169
+ if (e.detail === id) handler();
1170
+ };
1171
+ window.addEventListener("widget-open-settings", listener);
1172
+ return () => window.removeEventListener("widget-open-settings", listener);
1173
+ }, [handler, id]);
1174
+ }
1175
+ var _extraMenuItems = {};
1176
+ function useWindowMenuItem(label, onClick, icon) {
1177
+ const id = useContext(ModalIdContext);
1178
+ useEffect(() => {
1179
+ if (!id) return;
1180
+ if (!_extraMenuItems[id]) _extraMenuItems[id] = [];
1181
+ const item = { label, icon, onClick };
1182
+ _extraMenuItems[id].push(item);
1183
+ return () => {
1184
+ _extraMenuItems[id] = (_extraMenuItems[id] || []).filter((i) => i !== item);
1185
+ };
1186
+ }, [id, label, onClick, icon]);
1187
+ }
1188
+ function WindowTitle({ title }) {
1189
+ const ref = useRef(null);
1190
+ useEffect(() => {
1191
+ const handle = setTimeout(() => {
1192
+ const panel = ref.current?.closest("[data-modal-id]");
1193
+ const id = panel?.getAttribute("data-modal-id");
1194
+ if (id) window.dispatchEvent(new CustomEvent("window-title-update", { detail: { id, title } }));
1195
+ }, 0);
1196
+ return () => clearTimeout(handle);
1197
+ }, [title]);
1198
+ return /* @__PURE__ */ jsx("span", { ref, style: { display: "none" }, "aria-hidden": "true" });
1199
+ }
1200
+ function useWindowTitle(_title) {
1201
+ }
1202
+ function CopyButton({ text }) {
1203
+ const [copied, setCopied] = useState(false);
1204
+ const handleCopy = (e) => {
1205
+ e.stopPropagation();
1206
+ navigator.clipboard.writeText(text).then(() => {
1207
+ setCopied(true);
1208
+ setTimeout(() => setCopied(false), 1500);
1209
+ });
1210
+ };
1211
+ return /* @__PURE__ */ jsx(
1212
+ "button",
1213
+ {
1214
+ onClick: handleCopy,
1215
+ title: copied ? "Copied!" : `Copy ${text}`,
1216
+ className: "shrink-0 text-gray-400 hover:text-gray-600 transition-colors",
1217
+ children: copied ? /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 text-green-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" }) })
1218
+ }
1219
+ );
1220
+ }
1221
+ var ModalActionsContext = createContext(null);
1222
+ function useModalFooterOpen() {
1223
+ return useContext(ModalActionsContext)?.footerOpen ?? false;
1224
+ }
1225
+ function useEnclosingModalId() {
1226
+ return useContext(ModalIdContext);
1227
+ }
1228
+ function useModalActive() {
1229
+ const activeId = useSyncExternalStore(subscribeActive, getActiveModalId);
1230
+ const ctx = useContext(ModalActionsContext);
1231
+ return ctx?.active ?? (activationOrder.length <= 1 || activeId != null);
1232
+ }
1233
+ function ModalActions({ children, position = "right", quiet = false }) {
1234
+ const ctx = useContext(ModalActionsContext);
1235
+ const [target, setTarget] = useState(null);
1236
+ useEffect(() => {
1237
+ if (!ctx) return;
1238
+ const ref = position === "left" ? ctx.leftRef : ctx.rightRef;
1239
+ const check = () => {
1240
+ if (ref.current) {
1241
+ setTarget(ref.current);
1242
+ if (!quiet) ctx.notify();
1243
+ return true;
1244
+ }
1245
+ return false;
1246
+ };
1247
+ if (check()) return;
1248
+ const t = setInterval(() => {
1249
+ if (check()) clearInterval(t);
1250
+ }, 50);
1251
+ const cleanup = setTimeout(() => clearInterval(t), 2e3);
1252
+ return () => {
1253
+ clearInterval(t);
1254
+ clearTimeout(cleanup);
1255
+ };
1256
+ }, [ctx, position, quiet]);
1257
+ if (!target) return null;
1258
+ return createPortal(children, target);
1259
+ }
1260
+ function CancelButton({ onClick, children, className }) {
1261
+ const ctx = useContext(ModalActionsContext);
1262
+ const handleClick = async () => {
1263
+ if (ctx?.isDirty) {
1264
+ const { confirm: confirm2 } = await import('./ConfirmDialog-LJXMB45A.js');
1265
+ const ok = await confirm2({
1266
+ title: "Discard changes?",
1267
+ message: "You have unsaved changes. Are you sure you want to cancel? All changes will be lost.",
1268
+ confirmLabel: "Discard",
1269
+ cancelLabel: "Keep Editing",
1270
+ variant: "warning"
1271
+ });
1272
+ if (!ok) return;
1273
+ }
1274
+ onClick();
1275
+ };
1276
+ return /* @__PURE__ */ jsx(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ onClick: handleClick,
1281
+ className: className || "bg-white text-gray-700 border border-gray-300 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50",
1282
+ children: children || "Cancel"
1283
+ }
1284
+ );
1285
+ }
1286
+ var WP_KEY = "erp_window_positions";
1287
+ var _windowPositions = {};
1288
+ try {
1289
+ const stored = localStorage.getItem(WP_KEY);
1290
+ if (stored) _windowPositions = JSON.parse(stored);
1291
+ } catch {
1292
+ }
1293
+ var _saveTimer = null;
1294
+ function _savePositionsDebounced() {
1295
+ if (_saveTimer) clearTimeout(_saveTimer);
1296
+ _saveTimer = setTimeout(() => {
1297
+ try {
1298
+ localStorage.setItem(WP_KEY, JSON.stringify(_windowPositions));
1299
+ } catch {
1300
+ }
1301
+ }, 500);
1302
+ }
1303
+ var AO_KEY = "erp_activation_order";
1304
+ var _activationOrderKeys = [];
1305
+ try {
1306
+ const stored = localStorage.getItem(AO_KEY);
1307
+ const parsed = stored ? JSON.parse(stored) : null;
1308
+ if (Array.isArray(parsed) && parsed.every((k) => typeof k === "string")) _activationOrderKeys = parsed;
1309
+ } catch {
1310
+ }
1311
+ var _saveOrderTimer = null;
1312
+ function _saveOrderDebounced() {
1313
+ if (_saveOrderTimer) clearTimeout(_saveOrderTimer);
1314
+ _saveOrderTimer = setTimeout(() => {
1315
+ try {
1316
+ localStorage.setItem(AO_KEY, JSON.stringify(_activationOrderKeys));
1317
+ } catch {
1318
+ }
1319
+ }, 500);
1320
+ }
1321
+ function setWindowDefaultPosition(key, pos) {
1322
+ if (_windowPositions[key]) return;
1323
+ _windowPositions[key] = pos;
1324
+ _savePositionsDebounced();
1325
+ }
1326
+ function getWindowPosition(key) {
1327
+ return _windowPositions[key] ? { ..._windowPositions[key] } : null;
1328
+ }
1329
+ function setWindowPosition(key, pos) {
1330
+ _windowPositions[key] = pos;
1331
+ _savePositionsDebounced();
1332
+ }
1333
+ function forgetMaximizedWindowBoxes() {
1334
+ const workArea2 = computeMaximizedBox();
1335
+ const sidebarReserved = isSidebarStripReserved();
1336
+ let changed = false;
1337
+ for (const key of Object.keys(_windowPositions)) {
1338
+ if (boxFillsWorkArea(_windowPositions[key], workArea2, sidebarReserved)) {
1339
+ delete _windowPositions[key];
1340
+ changed = true;
1341
+ }
1342
+ }
1343
+ if (changed) _savePositionsDebounced();
1344
+ }
1345
+ var sizeDefaults = {
1346
+ sm: 384,
1347
+ md: 512,
1348
+ lg: 672,
1349
+ xl: 896,
1350
+ "2xl": 1152,
1351
+ "3xl": 1408
1352
+ };
1353
+ var AUTO_WIDTH_WATCH_MS = 4e3;
1354
+ var readDefaultWindowSize = () => {
1355
+ if (typeof document === "undefined") return "large";
1356
+ const v = getComputedStyle(document.documentElement).getPropertyValue("--default-window-size")?.trim();
1357
+ return v === "small" || v === "medium" || v === "maximized" ? v : "large";
1358
+ };
1359
+ var windowHeightScale = {
1360
+ small: 0.8,
1361
+ medium: 1,
1362
+ large: 1.25,
1363
+ maximized: 1.25
1364
+ };
1365
+ var activationOrder = [];
1366
+ var activeListeners = /* @__PURE__ */ new Set();
1367
+ var _minimizedIds = /* @__PURE__ */ new Set();
1368
+ var _activeVersion = 0;
1369
+ function getActiveVersion() {
1370
+ return _activeVersion;
1371
+ }
1372
+ function notifyActive() {
1373
+ _activeVersion++;
1374
+ activeListeners.forEach((fn) => fn());
1375
+ }
1376
+ var _modalIdByKey = /* @__PURE__ */ new Map();
1377
+ var _keyByModalId = /* @__PURE__ */ new Map();
1378
+ function _insertModalIdForKey(modalId, key) {
1379
+ const savedIdx = _activationOrderKeys.indexOf(key);
1380
+ if (savedIdx === -1) {
1381
+ activationOrder.push(modalId);
1382
+ return;
1383
+ }
1384
+ let insertAt = activationOrder.length;
1385
+ for (let i = 0; i < activationOrder.length; i++) {
1386
+ const otherKey = _keyByModalId.get(activationOrder[i]);
1387
+ if (!otherKey) continue;
1388
+ const otherIdx = _activationOrderKeys.indexOf(otherKey);
1389
+ if (otherIdx > savedIdx) {
1390
+ insertAt = i;
1391
+ break;
1392
+ }
1393
+ }
1394
+ activationOrder.splice(insertAt, 0, modalId);
1395
+ }
1396
+ var _frontRequest = null;
1397
+ function _honourFrontRequest(modalId) {
1398
+ _frontRequest = null;
1399
+ activateModal(modalId);
1400
+ }
1401
+ function requestWindowFront(windowKey) {
1402
+ _frontRequest = windowKey;
1403
+ const modalId = _modalIdByKey.get(windowKey);
1404
+ if (modalId) _honourFrontRequest(modalId);
1405
+ }
1406
+ function cancelWindowFront(windowKey) {
1407
+ if (_frontRequest === windowKey) _frontRequest = null;
1408
+ }
1409
+ function mountModal(modalId, key) {
1410
+ if (!key) {
1411
+ activationOrder.push(modalId);
1412
+ } else {
1413
+ _modalIdByKey.set(key, modalId);
1414
+ _keyByModalId.set(modalId, key);
1415
+ _insertModalIdForKey(modalId, key);
1416
+ if (_activationOrderKeys.indexOf(key) === -1) {
1417
+ _activationOrderKeys.push(key);
1418
+ _saveOrderDebounced();
1419
+ }
1420
+ }
1421
+ notifyActive();
1422
+ window.dispatchEvent(new CustomEvent("modal-reorder"));
1423
+ if (key && _frontRequest === key) _honourFrontRequest(modalId);
1424
+ }
1425
+ function unmountModal(modalId) {
1426
+ const idx = activationOrder.indexOf(modalId);
1427
+ if (idx !== -1) {
1428
+ activationOrder.splice(idx, 1);
1429
+ notifyActive();
1430
+ }
1431
+ _minimizedIds.delete(modalId);
1432
+ const key = _keyByModalId.get(modalId);
1433
+ if (key) {
1434
+ _keyByModalId.delete(modalId);
1435
+ _modalIdByKey.delete(key);
1436
+ }
1437
+ }
1438
+ function activateModal(id) {
1439
+ const idx = activationOrder.indexOf(id);
1440
+ if (idx !== -1) activationOrder.splice(idx, 1);
1441
+ activationOrder.push(id);
1442
+ _minimizedIds.delete(id);
1443
+ const key = _keyByModalId.get(id);
1444
+ if (key) {
1445
+ const kidx = _activationOrderKeys.indexOf(key);
1446
+ if (kidx !== -1) _activationOrderKeys.splice(kidx, 1);
1447
+ _activationOrderKeys.push(key);
1448
+ _saveOrderDebounced();
1449
+ if (_frontRequest && _frontRequest !== key) _frontRequest = null;
1450
+ }
1451
+ notifyActive();
1452
+ window.dispatchEvent(new CustomEvent("modal-reorder"));
1453
+ }
1454
+ function _minimizeModal(modalId) {
1455
+ const idx = activationOrder.indexOf(modalId);
1456
+ if (idx !== -1) activationOrder.splice(idx, 1);
1457
+ _minimizedIds.add(modalId);
1458
+ const key = _keyByModalId.get(modalId);
1459
+ if (key) {
1460
+ const kidx = _activationOrderKeys.indexOf(key);
1461
+ if (kidx !== -1) {
1462
+ _activationOrderKeys.splice(kidx, 1);
1463
+ _saveOrderDebounced();
1464
+ }
1465
+ }
1466
+ notifyActive();
1467
+ window.dispatchEvent(new CustomEvent("modal-reorder"));
1468
+ }
1469
+ var widgetIds = /* @__PURE__ */ new Set();
1470
+ function deactivateAllModals() {
1471
+ for (let i = activationOrder.length - 1; i >= 0; i--) {
1472
+ if (!widgetIds.has(activationOrder[i])) activationOrder.splice(i, 1);
1473
+ }
1474
+ _activationOrderKeys = _activationOrderKeys.filter((k) => {
1475
+ const mid = _modalIdByKey.get(k);
1476
+ return mid != null && widgetIds.has(mid);
1477
+ });
1478
+ _saveOrderDebounced();
1479
+ notifyActive();
1480
+ window.dispatchEvent(new CustomEvent("modal-reorder"));
1481
+ }
1482
+ var EDGE_THRESHOLD = 8;
1483
+ var CORNER_THRESHOLD = 32;
1484
+ function workArea() {
1485
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
1486
+ const taskbarW = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-width")) || 0;
1487
+ const tbPos = getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom";
1488
+ const x = tbPos === "left" ? taskbarW : 0;
1489
+ const y = tbPos === "top" ? taskbarH : 0;
1490
+ const w = window.innerWidth - (tbPos === "left" || tbPos === "right" ? taskbarW : 0);
1491
+ const h = window.innerHeight - (tbPos === "top" || tbPos === "bottom" ? taskbarH : 0);
1492
+ return { x, y, w, h };
1493
+ }
1494
+ var CAPTION_H = 40;
1495
+ var MIN_VISIBLE_X = 80;
1496
+ var MIN_W = 384;
1497
+ var MIN_H = 400;
1498
+ function clampReachable(box, a = workArea()) {
1499
+ const x = Math.min(Math.max(box.x, a.x + MIN_VISIBLE_X - box.w), a.x + a.w - MIN_VISIBLE_X);
1500
+ const y = Math.min(Math.max(box.y, a.y), a.y + a.h - CAPTION_H);
1501
+ return x === box.x && y === box.y ? box : { ...box, x, y };
1502
+ }
1503
+ function clampFullyVisible(box, a = workArea()) {
1504
+ const w = Math.min(box.w, a.w);
1505
+ const h = Math.min(box.h, a.h);
1506
+ return {
1507
+ w,
1508
+ h,
1509
+ x: Math.min(Math.max(box.x, a.x), a.x + a.w - w),
1510
+ y: Math.min(Math.max(box.y, a.y), a.y + a.h - h)
1511
+ };
1512
+ }
1513
+ function isPanelFullyVisible(panel) {
1514
+ const r = panel.getBoundingClientRect();
1515
+ if (r.width === 0 || r.height === 0) return true;
1516
+ const a = workArea();
1517
+ const EPS = 1;
1518
+ return r.left >= a.x - EPS && r.top >= a.y - EPS && r.right <= a.x + a.w + EPS && r.bottom <= a.y + a.h + EPS;
1519
+ }
1520
+ function panelOffscreenBearing(panel) {
1521
+ if (isPanelFullyVisible(panel)) return null;
1522
+ const r = panel.getBoundingClientRect();
1523
+ const a = workArea();
1524
+ const dx = r.left + r.width / 2 - (a.x + a.w / 2);
1525
+ const dy = r.top + r.height / 2 - (a.y + a.h / 2);
1526
+ return Math.atan2(dy, dx) * 180 / Math.PI;
1527
+ }
1528
+ function revealWindow(windowKey) {
1529
+ window.dispatchEvent(new CustomEvent("modal-reveal", { detail: { windowKey } }));
1530
+ }
1531
+ function calcSnapBox(zone) {
1532
+ const a = workArea();
1533
+ const halfW = Math.floor(a.w / 2);
1534
+ const halfH = Math.floor(a.h / 2);
1535
+ switch (zone) {
1536
+ case "top":
1537
+ return { x: a.x, y: a.y, w: a.w, h: a.h };
1538
+ case "left":
1539
+ return { x: a.x, y: a.y, w: halfW, h: a.h };
1540
+ case "right":
1541
+ return { x: a.x + halfW, y: a.y, w: a.w - halfW, h: a.h };
1542
+ case "tl":
1543
+ return { x: a.x, y: a.y, w: halfW, h: halfH };
1544
+ case "tr":
1545
+ return { x: a.x + halfW, y: a.y, w: a.w - halfW, h: halfH };
1546
+ case "bl":
1547
+ return { x: a.x, y: a.y + halfH, w: halfW, h: a.h - halfH };
1548
+ case "br":
1549
+ return { x: a.x + halfW, y: a.y + halfH, w: a.w - halfW, h: a.h - halfH };
1550
+ }
1551
+ }
1552
+ function detectSnapZone(clientX, clientY) {
1553
+ const a = workArea();
1554
+ const nearLeft = clientX <= a.x + EDGE_THRESHOLD;
1555
+ const nearRight = clientX >= a.x + a.w - EDGE_THRESHOLD;
1556
+ const nearTop = clientY <= a.y + EDGE_THRESHOLD;
1557
+ const nearBottom = clientY >= a.y + a.h - EDGE_THRESHOLD;
1558
+ const cornerLeft = clientX <= a.x + CORNER_THRESHOLD;
1559
+ const cornerRight = clientX >= a.x + a.w - CORNER_THRESHOLD;
1560
+ const cornerTop = clientY <= a.y + CORNER_THRESHOLD;
1561
+ const cornerBottom = clientY >= a.y + a.h - CORNER_THRESHOLD;
1562
+ if (nearTop && cornerLeft) return "tl";
1563
+ if (nearTop && cornerRight) return "tr";
1564
+ if (nearBottom && cornerLeft) return "bl";
1565
+ if (nearBottom && cornerRight) return "br";
1566
+ if (cornerTop && nearLeft) return "tl";
1567
+ if (cornerTop && nearRight) return "tr";
1568
+ if (cornerBottom && nearLeft) return "bl";
1569
+ if (cornerBottom && nearRight) return "br";
1570
+ if (nearTop) return "top";
1571
+ if (nearLeft) return "left";
1572
+ if (nearRight) return "right";
1573
+ return null;
1574
+ }
1575
+ var snapPreviewEl = null;
1576
+ function getSnapPreviewEl() {
1577
+ if (!snapPreviewEl) {
1578
+ snapPreviewEl = document.createElement("div");
1579
+ snapPreviewEl.style.cssText = [
1580
+ "position: fixed",
1581
+ "pointer-events: none",
1582
+ "z-index: 40",
1583
+ "border-radius: 8px",
1584
+ "background: rgba(59, 130, 246, 0.18)",
1585
+ "border: 2px solid rgb(59, 130, 246)",
1586
+ "transition: left 120ms ease, top 120ms ease, width 120ms ease, height 120ms ease, opacity 120ms ease",
1587
+ "opacity: 0",
1588
+ "display: none"
1589
+ ].join(";");
1590
+ document.body.appendChild(snapPreviewEl);
1591
+ }
1592
+ return snapPreviewEl;
1593
+ }
1594
+ function showSnapPreview(box) {
1595
+ const el = getSnapPreviewEl();
1596
+ el.style.display = "block";
1597
+ el.style.left = `${box.x}px`;
1598
+ el.style.top = `${box.y}px`;
1599
+ el.style.width = `${box.w}px`;
1600
+ el.style.height = `${box.h}px`;
1601
+ el.style.opacity = "1";
1602
+ }
1603
+ function hideSnapPreview() {
1604
+ if (!snapPreviewEl) return;
1605
+ snapPreviewEl.style.opacity = "0";
1606
+ snapPreviewEl.style.display = "none";
1607
+ }
1608
+ var SNAP_PICKER_ZONES = [
1609
+ { zone: "left", labelKey: "snapLeft", region: "inset-y-0 left-0 w-1/2" },
1610
+ { zone: "right", labelKey: "snapRight", region: "inset-y-0 right-0 w-1/2" },
1611
+ { zone: "tl", labelKey: "snapTopLeft", region: "top-0 left-0 h-1/2 w-1/2" },
1612
+ { zone: "tr", labelKey: "snapTopRight", region: "top-0 right-0 h-1/2 w-1/2" },
1613
+ { zone: "bl", labelKey: "snapBottomLeft", region: "bottom-0 left-0 h-1/2 w-1/2" },
1614
+ { zone: "br", labelKey: "snapBottomRight", region: "bottom-0 right-0 h-1/2 w-1/2" }
1615
+ ];
1616
+ var SNAP_PICKER_OPEN_MS = 350;
1617
+ var SNAP_PICKER_CLOSE_MS = 200;
1618
+ function SnapLayoutsButton({ maximized, onToggleMax, onSnap, buttonClassName, strings }) {
1619
+ const [open, setOpen] = useState(false);
1620
+ const openTimer = useRef(void 0);
1621
+ const closeTimer = useRef(void 0);
1622
+ useEffect(() => () => {
1623
+ clearTimeout(openTimer.current);
1624
+ clearTimeout(closeTimer.current);
1625
+ }, []);
1626
+ const armOpen = () => {
1627
+ clearTimeout(closeTimer.current);
1628
+ openTimer.current = setTimeout(() => setOpen(true), SNAP_PICKER_OPEN_MS);
1629
+ };
1630
+ const armClose = () => {
1631
+ clearTimeout(openTimer.current);
1632
+ closeTimer.current = setTimeout(() => setOpen(false), SNAP_PICKER_CLOSE_MS);
1633
+ };
1634
+ return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex", onMouseEnter: armOpen, onMouseLeave: armClose, children: [
1635
+ /* @__PURE__ */ jsx(
1636
+ "button",
1637
+ {
1638
+ onClick: onToggleMax,
1639
+ onFocus: armOpen,
1640
+ onBlur: armClose,
1641
+ title: maximized ? strings.windowed : strings.maximize,
1642
+ className: buttonClassName,
1643
+ children: maximized ? "\u2750" : "\u2922"
1644
+ }
1645
+ ),
1646
+ open && /* @__PURE__ */ jsx(
1647
+ "div",
1648
+ {
1649
+ role: "group",
1650
+ "aria-label": strings.snapLayouts,
1651
+ onMouseEnter: () => clearTimeout(closeTimer.current),
1652
+ onMouseLeave: armClose,
1653
+ className: "absolute right-0 top-full z-30 mt-1 grid w-max grid-cols-2 gap-1 rounded-lg border border-gray-200 bg-white p-1.5 shadow-lg",
1654
+ children: SNAP_PICKER_ZONES.map(({ zone, labelKey, region }) => /* @__PURE__ */ jsx(
1655
+ "button",
1656
+ {
1657
+ type: "button",
1658
+ "aria-label": strings[labelKey],
1659
+ title: strings[labelKey],
1660
+ onClick: () => {
1661
+ setOpen(false);
1662
+ onSnap(zone);
1663
+ },
1664
+ onFocus: () => clearTimeout(closeTimer.current),
1665
+ onBlur: armClose,
1666
+ className: "relative h-7 w-11 rounded border border-gray-300 bg-gray-50 hover:border-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400",
1667
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `absolute rounded-sm bg-blue-500/70 ${region}`, style: { margin: 2 } })
1668
+ },
1669
+ zone
1670
+ ))
1671
+ }
1672
+ )
1673
+ ] });
1674
+ }
1675
+ var GESTURE_STYLE_ID = "rosh-gesture-style";
1676
+ function ensureGestureStyle() {
1677
+ if (typeof document === "undefined" || document.getElementById(GESTURE_STYLE_ID)) return;
1678
+ const style = document.createElement("style");
1679
+ style.id = GESTURE_STYLE_ID;
1680
+ style.textContent = // Promote every window to its own compositor layer for the gesture so
1681
+ // dragging the foreground window doesn't repaint the ones behind it. We do
1682
+ // NOT drop backdrop-blur — every window keeps its frosted glass while a
1683
+ // drag/resize is in progress (only the grabbed window re-samples, cheaply).
1684
+ "body.rosh-gesturing [data-modal-panel]{will-change:transform}";
1685
+ document.head.appendChild(style);
1686
+ }
1687
+ var RESIZE_CURSOR = {
1688
+ se: "nwse-resize",
1689
+ nw: "nwse-resize",
1690
+ sw: "nesw-resize",
1691
+ ne: "nesw-resize",
1692
+ n: "ns-resize",
1693
+ s: "ns-resize",
1694
+ e: "ew-resize",
1695
+ w: "ew-resize"
1696
+ };
1697
+ var DRAG_THRESHOLD = 4;
1698
+ function beginPointerGesture(handle, pointerId, cursor, kind) {
1699
+ ensureGestureStyle();
1700
+ try {
1701
+ handle.setPointerCapture(pointerId);
1702
+ } catch {
1703
+ }
1704
+ document.body.classList.add("rosh-gesturing");
1705
+ const shield = document.createElement("div");
1706
+ shield.setAttribute("data-drag-shield", "");
1707
+ shield.style.cssText = `position:fixed;inset:0;z-index:2147483647;cursor:${cursor};background:transparent;touch-action:none`;
1708
+ document.body.appendChild(shield);
1709
+ const endMark = beginWindowGesture(kind);
1710
+ let done = false;
1711
+ return () => {
1712
+ if (done) return;
1713
+ done = true;
1714
+ endMark();
1715
+ try {
1716
+ handle.releasePointerCapture(pointerId);
1717
+ } catch {
1718
+ }
1719
+ shield.remove();
1720
+ document.body.classList.remove("rosh-gesturing");
1721
+ };
1722
+ }
1723
+ if (typeof window !== "undefined") window.addEventListener("deactivate-all-modals", deactivateAllModals);
1724
+ function getZForModal(id) {
1725
+ const idx = activationOrder.indexOf(id);
1726
+ if (idx === -1) return -1;
1727
+ return Z_LAYERS.window + idx * Z_LAYERS.windowStep;
1728
+ }
1729
+ function getActiveModalId() {
1730
+ return activationOrder[activationOrder.length - 1] || null;
1731
+ }
1732
+ function subscribeActive(cb) {
1733
+ activeListeners.add(cb);
1734
+ return () => {
1735
+ activeListeners.delete(cb);
1736
+ };
1737
+ }
1738
+ var MODAL_CLOSE_REQUEST_EVENT = "modal-close-request";
1739
+ function requestModalClose(windowKey) {
1740
+ window.dispatchEvent(new CustomEvent(MODAL_CLOSE_REQUEST_EVENT, {
1741
+ detail: { windowKey }
1742
+ }));
1743
+ }
1744
+ function minimizeWindowByKey(windowKey) {
1745
+ const modalId = _modalIdByKey.get(windowKey);
1746
+ if (modalId) _minimizeModal(modalId);
1747
+ }
1748
+ function useIsActiveModal(modalId) {
1749
+ const activeId = useSyncExternalStore(subscribeActive, getActiveModalId);
1750
+ return activationOrder.length <= 1 || activeId === modalId;
1751
+ }
1752
+ function useIsMinimizedModal(modalId) {
1753
+ return useSyncExternalStore(subscribeActive, () => _minimizedIds.has(modalId));
1754
+ }
1755
+ function useIsActiveWindow(windowKey) {
1756
+ useSyncExternalStore(subscribeActive, getActiveVersion);
1757
+ const ctx = useContext(ModalActionsContext);
1758
+ if (activationOrder.length <= 1) return true;
1759
+ if (windowKey) {
1760
+ const modalId = _modalIdByKey.get(windowKey);
1761
+ if (modalId != null) return getActiveModalId() === modalId;
1762
+ }
1763
+ return ctx?.active ?? getActiveModalId() != null;
1764
+ }
1765
+ var _exposeOn = false;
1766
+ var _exposeListeners = /* @__PURE__ */ new Set();
1767
+ function _notifyExpose() {
1768
+ _exposeListeners.forEach((fn) => fn());
1769
+ }
1770
+ function subscribeExpose(fn) {
1771
+ _exposeListeners.add(fn);
1772
+ return () => _exposeListeners.delete(fn);
1773
+ }
1774
+ function getExposeState() {
1775
+ return _exposeOn;
1776
+ }
1777
+ function setExposeState(v) {
1778
+ if (_exposeOn === v) return;
1779
+ _exposeOn = v;
1780
+ _notifyExpose();
1781
+ }
1782
+ function toggleExposeMode() {
1783
+ setExposeState(!_exposeOn);
1784
+ }
1785
+ function exitExposeMode() {
1786
+ setExposeState(false);
1787
+ }
1788
+ var _exposeExitFocusId = null;
1789
+ var _exposeExitListeners = /* @__PURE__ */ new Set();
1790
+ function subscribeExposeExitFocus(fn) {
1791
+ _exposeExitListeners.add(fn);
1792
+ return () => _exposeExitListeners.delete(fn);
1793
+ }
1794
+ function getExposeExitFocus() {
1795
+ return _exposeExitFocusId;
1796
+ }
1797
+ function setExposeExitFocus(id) {
1798
+ if (_exposeExitFocusId === id) return;
1799
+ _exposeExitFocusId = id;
1800
+ _exposeExitListeners.forEach((fn) => fn());
1801
+ }
1802
+ var _exposeHighlightId = null;
1803
+ var _exposeHighlightListeners = /* @__PURE__ */ new Set();
1804
+ function subscribeExposeHighlight(fn) {
1805
+ _exposeHighlightListeners.add(fn);
1806
+ return () => _exposeHighlightListeners.delete(fn);
1807
+ }
1808
+ function getExposeHighlight() {
1809
+ return _exposeHighlightId;
1810
+ }
1811
+ function setExposeHighlight(id) {
1812
+ if (_exposeHighlightId === id) return;
1813
+ _exposeHighlightId = id;
1814
+ _exposeHighlightListeners.forEach((fn) => fn());
1815
+ }
1816
+ function commitExposeHighlight() {
1817
+ if (!_exposeHighlightId) {
1818
+ setExposeState(false);
1819
+ return;
1820
+ }
1821
+ const id = _exposeHighlightId;
1822
+ setExposeExitFocus(id);
1823
+ setExposeHighlight(null);
1824
+ setExposeState(false);
1825
+ }
1826
+ function triggerSplitView() {
1827
+ setExposeState(!_exposeOn);
1828
+ window.dispatchEvent(new CustomEvent("modal-split-view"));
1829
+ }
1830
+ if (typeof window !== "undefined") {
1831
+ window.addEventListener("keydown", (e) => {
1832
+ if (_exposeOn && e.key === "Escape") {
1833
+ setExposeHighlight(null);
1834
+ setExposeState(false);
1835
+ }
1836
+ });
1837
+ }
1838
+ function computeExposeTile(modalId) {
1839
+ if (typeof document === "undefined") return null;
1840
+ const allPanels = Array.from(document.querySelectorAll("[data-modal-panel]"));
1841
+ const tileable = allPanels.filter((p) => {
1842
+ const el = p;
1843
+ if (el.hasAttribute("data-utility") || el.hasAttribute("data-widget")) return false;
1844
+ if (el.style.display === "none") return false;
1845
+ return true;
1846
+ });
1847
+ tileable.sort((a2, b) => (a2.getAttribute("data-modal-id") || "").localeCompare(b.getAttribute("data-modal-id") || ""));
1848
+ const myIdx = tileable.findIndex((p) => p.getAttribute("data-modal-id") === modalId);
1849
+ const count = tileable.length;
1850
+ if (myIdx < 0 || count < 1) return null;
1851
+ const cols = Math.ceil(Math.sqrt(count));
1852
+ const rows = Math.ceil(count / cols);
1853
+ const myRow = Math.floor(myIdx / cols);
1854
+ const myCol = myIdx % cols;
1855
+ const lastRowCount = count - cols * (rows - 1);
1856
+ const isLastRow = myRow === rows - 1 && lastRowCount < cols;
1857
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
1858
+ const taskbarW = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-width")) || 0;
1859
+ const tbPos = (getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position") || "bottom").trim();
1860
+ const xOffset = tbPos === "left" ? taskbarW : 0;
1861
+ const xRight = tbPos === "right" ? taskbarW : 0;
1862
+ const yOffset = tbPos === "top" ? taskbarH : 0;
1863
+ const yBottom = tbPos === "top" || tbPos === "left" || tbPos === "right" ? 0 : taskbarH;
1864
+ const a = {
1865
+ x: xOffset,
1866
+ y: yOffset,
1867
+ w: window.innerWidth - xOffset - xRight,
1868
+ h: window.innerHeight - yOffset - yBottom
1869
+ };
1870
+ const gapX = 20;
1871
+ const gapY = 14;
1872
+ const labelH = 22;
1873
+ const cellW = (a.w - gapX * (cols + 1)) / cols;
1874
+ const cellH = (a.h - gapY * (rows + 1) - labelH * rows) / rows;
1875
+ const lastRowOffsetX = isLastRow ? (cols - lastRowCount) * (cellW + gapX) / 2 : 0;
1876
+ const tileX = a.x + gapX + myCol * (cellW + gapX) + lastRowOffsetX;
1877
+ const tileY = a.y + gapY + myRow * (cellH + gapY + labelH);
1878
+ return { x: tileX, y: tileY, w: cellW, h: cellH };
1879
+ }
1880
+ function ExposeBackdrop() {
1881
+ const on = useSyncExternalStore(subscribeExpose, getExposeState);
1882
+ const [mounted, setMounted] = useState(on);
1883
+ const [visible, setVisible] = useState(on);
1884
+ useEffect(() => {
1885
+ if (on) {
1886
+ setMounted(true);
1887
+ const id = requestAnimationFrame(() => setVisible(true));
1888
+ return () => cancelAnimationFrame(id);
1889
+ }
1890
+ setVisible(false);
1891
+ const t = setTimeout(() => setMounted(false), 280);
1892
+ return () => clearTimeout(t);
1893
+ }, [on]);
1894
+ if (!mounted || typeof document === "undefined") return null;
1895
+ return createPortal(
1896
+ /* @__PURE__ */ jsx(
1897
+ "div",
1898
+ {
1899
+ onMouseDown: (e) => {
1900
+ e.stopPropagation();
1901
+ setExposeState(false);
1902
+ },
1903
+ style: {
1904
+ position: "fixed",
1905
+ inset: 0,
1906
+ background: "rgba(0,0,0,0.55)",
1907
+ backdropFilter: "blur(2px)",
1908
+ WebkitBackdropFilter: "blur(2px)",
1909
+ zIndex: 2e3,
1910
+ cursor: "pointer",
1911
+ opacity: visible ? 1 : 0,
1912
+ transition: "opacity 260ms cubic-bezier(0.2, 0.8, 0.2, 1)",
1913
+ pointerEvents: visible ? "auto" : "none"
1914
+ }
1915
+ }
1916
+ ),
1917
+ document.body
1918
+ );
1919
+ }
1920
+ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = false, onNext, onPrev, footer, bodyScroll, onMinimize, initialBox, actions, actionsLeft, editing = false, allowPinOnTop, initialPosition, widget, compact, appStyle, flushBody, autoHeight, autoMinHeight, autoWidth, widgetMenu, dimensions, windowKey, openedFromKey, accentRgb, children }) {
1921
+ const isMobile = useIsMobile();
1922
+ const [swipeX, setSwipeX] = useState(0);
1923
+ const [swipeDragging, setSwipeDragging] = useState(false);
1924
+ const swipeStartRef = useRef(null);
1925
+ const swipeXRef = useRef(0);
1926
+ useEffect(() => {
1927
+ swipeXRef.current = swipeX;
1928
+ }, [swipeX]);
1929
+ const handleEdgePointerDown = useCallback((e) => {
1930
+ if (!isMobile) return;
1931
+ swipeStartRef.current = { startX: e.clientX, startY: e.clientY, pointerId: e.pointerId };
1932
+ setSwipeDragging(true);
1933
+ setSwipingParentKey(openedFromKey ?? null);
1934
+ }, [isMobile, openedFromKey]);
1935
+ const swipingParentKey = useSyncExternalStore(subscribeSwipingParentKey, getSwipingParentKey);
1936
+ useEffect(() => {
1937
+ if (!swipeDragging) return;
1938
+ const onMove = (ev) => {
1939
+ if (!swipeStartRef.current) return;
1940
+ const dx = ev.clientX - swipeStartRef.current.startX;
1941
+ const dy = Math.abs(ev.clientY - swipeStartRef.current.startY);
1942
+ if (dy > Math.abs(dx) + 12) {
1943
+ swipeStartRef.current = null;
1944
+ setSwipeDragging(false);
1945
+ setSwipeX(0);
1946
+ setSwipingParentKey(null);
1947
+ return;
1948
+ }
1949
+ setSwipeX(Math.max(0, dx));
1950
+ };
1951
+ const onUp = () => {
1952
+ if (!swipeStartRef.current) return;
1953
+ const threshold = window.innerWidth * 0.3;
1954
+ const past = swipeXRef.current > threshold;
1955
+ swipeStartRef.current = null;
1956
+ setSwipeDragging(false);
1957
+ if (past) {
1958
+ setSwipeX(window.innerWidth);
1959
+ setTimeout(() => {
1960
+ onClose();
1961
+ setSwipeX(0);
1962
+ setSwipingParentKey(null);
1963
+ }, 180);
1964
+ } else {
1965
+ setSwipeX(0);
1966
+ setSwipingParentKey(null);
1967
+ }
1968
+ };
1969
+ window.addEventListener("pointermove", onMove);
1970
+ window.addEventListener("pointerup", onUp);
1971
+ window.addEventListener("pointercancel", onUp);
1972
+ return () => {
1973
+ window.removeEventListener("pointermove", onMove);
1974
+ window.removeEventListener("pointerup", onUp);
1975
+ window.removeEventListener("pointercancel", onUp);
1976
+ };
1977
+ }, [swipeDragging]);
1978
+ const [displayTitle, setDisplayTitle] = useState(title);
1979
+ const shellStrings = useShellStrings();
1980
+ const winStr = shellStrings.window;
1981
+ useEffect(() => {
1982
+ setDisplayTitle(title);
1983
+ }, [title]);
1984
+ const titleTooltip = useMemo(() => extractTitleText(displayTitle).trim() || void 0, [displayTitle]);
1985
+ const [touched, setTouched] = useState(false);
1986
+ const touchedRef = useRef(false);
1987
+ const [pinnedOnTop, setPinnedOnTop] = useState(false);
1988
+ const [windowMenu, setWindowMenu] = useState(null);
1989
+ const shortcutSpec = useContext(WindowShortcutContext);
1990
+ const { prefs: shellPrefs, save: saveShellPrefs } = useShellPrefs();
1991
+ const accentStripeOn = !!shellPrefs.window_accent_stripe;
1992
+ const favDocs = shellPrefs.favorite_documents || [];
1993
+ const isOnDesktop = !!shortcutSpec && favDocs.some((d) => d.entityType === shortcutSpec.entityType && d.entityId === shortcutSpec.entityId);
1994
+ const toggleDesktopShortcut = useCallback(() => {
1995
+ if (!shortcutSpec) return;
1996
+ const next = isOnDesktop ? favDocs.filter((d) => !(d.entityType === shortcutSpec.entityType && d.entityId === shortcutSpec.entityId)) : [...favDocs, { entityType: shortcutSpec.entityType, entityId: shortcutSpec.entityId, label: shortcutSpec.label }];
1997
+ saveShellPrefs({ favorite_documents: next });
1998
+ }, [shortcutSpec, isOnDesktop, favDocs, saveShellPrefs]);
1999
+ const [ctxMenu, setCtxMenu] = useState(null);
2000
+ const [widgetAnchor, setWidgetAnchor] = useState(initialPosition === "top-right" ? "right" : "left");
2001
+ const closingRef = useRef(false);
2002
+ const panelRef = useRef(null);
2003
+ useLayoutEffect(() => {
2004
+ const el = panelRef.current;
2005
+ if (!el) return;
2006
+ if (accentRgb) el.style.setProperty("--window-accent-rgb", accentRgb);
2007
+ else el.style.removeProperty("--window-accent-rgb");
2008
+ }, [accentRgb]);
2009
+ const swallowInactiveClickRef = useRef(false);
2010
+ const bodyRef = useRef(null);
2011
+ const actionsRef = useRef(null);
2012
+ const actionsLeftRef = useRef(null);
2013
+ const [hasActions, setHasActions] = useState(false);
2014
+ const footerRef = useRef(null);
2015
+ const hasFooterContent = !!(footer || actions || actionsLeft) || hasActions;
2016
+ const undoCtx = useContext(UndoContext);
2017
+ const nestedInWindow = useContext(ModalIdContext) !== "";
2018
+ const showsUndo = !!undoCtx && !nestedInWindow && undoCtx.enabled && undoCtx.hasState && (editing || undoCtx.declaredEditable) && !undoCtx.handMounted && hasFooterContent && !widget && !compact && !appStyle && !isMobile;
2019
+ const fallbackIcon = /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75A2.25 2.25 0 016 4.5h12a2.25 2.25 0 012.25 2.25v10.5A2.25 2.25 0 0118 19.5H6a2.25 2.25 0 01-2.25-2.25V6.75z M3.75 9h16.5" }) });
2020
+ const effectiveIcon = icon && isValidElement(icon) ? cloneElement(icon, {
2021
+ className: `h-4 w-4 ${icon.props?.className ?? ""}`.trim()
2022
+ }) : icon ?? fallbackIcon;
2023
+ const renderIconButton = () => /* @__PURE__ */ jsx(
2024
+ "button",
2025
+ {
2026
+ onPointerDown: (e) => e.stopPropagation(),
2027
+ onClick: (e) => {
2028
+ e.stopPropagation();
2029
+ const rect = e.currentTarget.getBoundingClientRect();
2030
+ setWindowMenu((prev) => prev ? null : { x: rect.left, y: rect.bottom + 4 });
2031
+ },
2032
+ className: "shrink-0 p-0.5 rounded hover:bg-gray-200/50 transition-colors",
2033
+ title: "Window menu",
2034
+ children: effectiveIcon
2035
+ }
2036
+ );
2037
+ const padding = 40;
2038
+ const { minimize: globalMinimize, items: minimizedItems, restoreIfMinimized, openWindows } = useWindowManager();
2039
+ const modalId = useRef(`modal-${Math.random().toString(36).slice(2, 8)}`).current;
2040
+ useEffect(() => {
2041
+ if (widget) {
2042
+ widgetIds.add(modalId);
2043
+ return () => {
2044
+ widgetIds.delete(modalId);
2045
+ };
2046
+ }
2047
+ }, [widget, modalId]);
2048
+ useEffect(() => {
2049
+ const handler = (e) => {
2050
+ const detail = e.detail;
2051
+ if (detail?.id === modalId) setDisplayTitle(detail.title);
2052
+ };
2053
+ window.addEventListener("window-title-update", handler);
2054
+ return () => window.removeEventListener("window-title-update", handler);
2055
+ }, [modalId]);
2056
+ const [zIndex, setZIndex] = useState(Z_LAYERS.window);
2057
+ const isActive = useIsActiveModal(modalId);
2058
+ const isMinimized = useIsMinimizedModal(modalId);
2059
+ const exposeOn = useSyncExternalStore(subscribeExpose, getExposeState);
2060
+ const exposeExitFocusId = useSyncExternalStore(subscribeExposeExitFocus, getExposeExitFocus);
2061
+ const exposeHighlightId = useSyncExternalStore(subscribeExposeHighlight, getExposeHighlight);
2062
+ const isExposeTileable = !allowPinOnTop && !widget;
2063
+ const exposeActive = exposeOn && isExposeTileable;
2064
+ const [exposeHovered, setExposeHovered] = useState(false);
2065
+ const exposeKeyboardHighlight = exposeActive && exposeHighlightId === modalId;
2066
+ const [exposeExiting, setExposeExiting] = useState(false);
2067
+ const exposeExitRole = exposeExiting && isExposeTileable && exposeExitFocusId === modalId ? "picked" : null;
2068
+ const prevExposeActiveRef = useRef(exposeActive);
2069
+ useEffect(() => {
2070
+ if (prevExposeActiveRef.current && !exposeActive) {
2071
+ setExposeExiting(true);
2072
+ const t = setTimeout(() => {
2073
+ setExposeExiting(false);
2074
+ if (_exposeExitFocusId === modalId) setExposeExitFocus(null);
2075
+ }, 700);
2076
+ prevExposeActiveRef.current = exposeActive;
2077
+ return () => clearTimeout(t);
2078
+ }
2079
+ prevExposeActiveRef.current = exposeActive;
2080
+ }, [exposeActive, modalId]);
2081
+ useEffect(() => {
2082
+ if (!exposeActive) setExposeHovered(false);
2083
+ }, [exposeActive]);
2084
+ useEffect(() => {
2085
+ const r = actionsRef.current;
2086
+ const l = actionsLeftRef.current;
2087
+ if (!r && !l) return;
2088
+ const counted = (el) => el ? Array.from(el.children).filter((c) => c.dataset.undoControls !== "shell").length : 0;
2089
+ const check = () => setHasActions(counted(r) + counted(l) > 0);
2090
+ check();
2091
+ const obs = new MutationObserver(check);
2092
+ if (r) obs.observe(r, { childList: true });
2093
+ if (l) obs.observe(l, { childList: true });
2094
+ return () => obs.disconnect();
2095
+ }, [open]);
2096
+ useEffect(() => {
2097
+ const handler = (e) => {
2098
+ const btn = e.target.closest('button[type="submit"]');
2099
+ if (btn && panelRef.current) {
2100
+ const form = panelRef.current.querySelector("form");
2101
+ if (form) {
2102
+ e.preventDefault();
2103
+ form.requestSubmit();
2104
+ }
2105
+ }
2106
+ };
2107
+ const r = actionsRef.current;
2108
+ const l = actionsLeftRef.current;
2109
+ if (r) r.addEventListener("click", handler);
2110
+ if (l) l.addEventListener("click", handler);
2111
+ return () => {
2112
+ if (r) r.removeEventListener("click", handler);
2113
+ if (l) l.removeEventListener("click", handler);
2114
+ };
2115
+ }, [open]);
2116
+ const isNested = useRef(false);
2117
+ const calcMaximized = useCallback(computeMaximizedBox, []);
2118
+ const calcWindowed = useCallback(() => {
2119
+ const sw = 0;
2120
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
2121
+ const taskbarW = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-width")) || 0;
2122
+ const tbPos = getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom";
2123
+ const leftOffset = tbPos === "left" ? taskbarW : 0;
2124
+ const rightOffset = tbPos === "right" ? taskbarW : 0;
2125
+ const targetW = dimensions ? dimensions[0] : sizeDefaults[size] || 672;
2126
+ const availW = window.innerWidth - sw - leftOffset - rightOffset - padding * 2;
2127
+ const availH = window.innerHeight - taskbarH - padding * 2;
2128
+ const w = Math.min(targetW, availW);
2129
+ const heightScale = windowHeightScale[readDefaultWindowSize()];
2130
+ const h = dimensions ? Math.min(dimensions[1], availH) : (() => {
2131
+ const minH = 320;
2132
+ const maxH = (size === "sm" ? 500 : size === "md" ? 600 : size === "lg" ? 700 : size === "xl" ? 800 : 920) * heightScale;
2133
+ return Math.max(minH, Math.min(maxH, window.innerHeight - taskbarH - 80));
2134
+ })();
2135
+ const posMode = getComputedStyle(document.documentElement).getPropertyValue("--window-position")?.trim() || "cascade";
2136
+ const offset = posMode === "cascade" ? (activationOrder.length - 1) * 30 : 0;
2137
+ const maxOffsetX = availW - w;
2138
+ const maxOffsetY = availH - h;
2139
+ if (initialPosition === "top-right") {
2140
+ return { x: window.innerWidth - w - padding, y: padding, w, h };
2141
+ }
2142
+ if (initialPosition === "top-left") {
2143
+ return { x: sw + padding, y: padding, w, h };
2144
+ }
2145
+ return {
2146
+ x: sw + Math.max(padding, (availW - w) / 2 + padding) + Math.min(offset, Math.max(0, maxOffsetX)),
2147
+ y: Math.max(padding, (availH - h) / 2) + Math.min(offset, Math.max(0, maxOffsetY)),
2148
+ w,
2149
+ h
2150
+ };
2151
+ }, [size]);
2152
+ const interceptedRef = useRef(false);
2153
+ if (!open) interceptedRef.current = false;
2154
+ const boxKey = windowKey || copyText || null;
2155
+ useEffect(() => {
2156
+ if (!open) return;
2157
+ mountModal(modalId, boxKey);
2158
+ if (!windowKey) activateModal(modalId);
2159
+ isNested.current = false;
2160
+ setZIndex(getZForModal(modalId));
2161
+ const onReorder = () => {
2162
+ setZIndex(getZForModal(modalId));
2163
+ };
2164
+ const onSplitView = () => {
2165
+ };
2166
+ const addressesThisWindow = (detail) => {
2167
+ if (!detail) return false;
2168
+ if (detail.windowKey) return detail.windowKey === windowKey;
2169
+ if (!detail.label) return false;
2170
+ const titleEl = panelRef.current?.querySelector("[data-window-title]");
2171
+ return !!titleEl?.textContent?.includes(detail.label);
2172
+ };
2173
+ const onCenter = (e) => {
2174
+ if (!addressesThisWindow(e.detail)) return;
2175
+ activateModal(modalId);
2176
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
2177
+ const taskbarW = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-width")) || 0;
2178
+ const tbPos = getComputedStyle(document.documentElement).getPropertyValue("--taskbar-position")?.trim() || "bottom";
2179
+ const leftOff = tbPos === "left" ? taskbarW : 0;
2180
+ const rightOff = tbPos === "right" ? taskbarW : 0;
2181
+ const availW = window.innerWidth - leftOff - rightOff;
2182
+ const availH = window.innerHeight - taskbarH;
2183
+ const w = Math.min(boxRef.current.w, availW - 40);
2184
+ const h = Math.min(boxRef.current.h, availH - 40);
2185
+ const x = leftOff + (availW - w) / 2;
2186
+ const y = (tbPos === "top" ? taskbarH : 0) + (availH - h) / 2;
2187
+ setBox({ x, y, w, h });
2188
+ setMaximized(false);
2189
+ };
2190
+ const onCtxMenu = (e) => {
2191
+ const detail = e.detail;
2192
+ if (!addressesThisWindow(detail)) return;
2193
+ activateModal(modalId);
2194
+ setWindowMenu({ x: detail.x, y: detail.y });
2195
+ };
2196
+ const onReveal = (e) => {
2197
+ if (e.detail?.windowKey !== windowKey) return;
2198
+ activateModal(modalId);
2199
+ const cur = boxRef.current;
2200
+ const next = clampFullyVisible(cur);
2201
+ if (next.x === cur.x && next.y === cur.y && next.w === cur.w && next.h === cur.h) return;
2202
+ const panel = panelRef.current;
2203
+ const stillness = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2204
+ if (panel && !stillness) {
2205
+ let settled = false;
2206
+ const done = () => {
2207
+ if (settled) return;
2208
+ settled = true;
2209
+ panel.style.transition = "";
2210
+ window.dispatchEvent(new CustomEvent("modal-geometry"));
2211
+ };
2212
+ panel.style.transition = "left .28s ease, top .28s ease, width .28s ease, height .28s ease";
2213
+ panel.addEventListener("transitionend", done, { once: true });
2214
+ window.setTimeout(done, 400);
2215
+ }
2216
+ setBox(next);
2217
+ };
2218
+ window.addEventListener("modal-reorder", onReorder);
2219
+ window.addEventListener("modal-split-view", onSplitView);
2220
+ window.addEventListener("modal-center", onCenter);
2221
+ window.addEventListener("modal-reveal", onReveal);
2222
+ window.addEventListener("modal-context-menu", onCtxMenu);
2223
+ return () => {
2224
+ unmountModal(modalId);
2225
+ window.removeEventListener("modal-reorder", onReorder);
2226
+ window.removeEventListener("modal-split-view", onSplitView);
2227
+ window.removeEventListener("modal-center", onCenter);
2228
+ window.removeEventListener("modal-reveal", onReveal);
2229
+ window.removeEventListener("modal-context-menu", onCtxMenu);
2230
+ window.dispatchEvent(new CustomEvent("modal-reorder"));
2231
+ };
2232
+ }, [open, modalId, boxKey, calcMaximized]);
2233
+ const openMaximized = !widget && readDefaultWindowSize() === "maximized";
2234
+ const [box, setBox] = useState(() => {
2235
+ if (boxKey && _windowPositions[boxKey]) {
2236
+ const saved = { ..._windowPositions[boxKey] };
2237
+ if (dimensions) {
2238
+ saved.w = dimensions[0];
2239
+ if (!autoHeight) saved.h = dimensions[1];
2240
+ }
2241
+ return clampReachable(saved);
2242
+ }
2243
+ if (initialBox) return { x: initialBox.x, y: initialBox.y, w: initialBox.w, h: initialBox.h };
2244
+ return openMaximized ? calcMaximized() : calcWindowed();
2245
+ });
2246
+ const [autoHeightResolved, setAutoHeightResolved] = useState(() => !autoHeight);
2247
+ const [alwaysMaximizedRaw, setAlwaysMaximizedRaw] = useState(() => readAlwaysMaximizedFlag());
2248
+ useEffect(() => {
2249
+ const refresh = () => setAlwaysMaximizedRaw(readAlwaysMaximizedFlag());
2250
+ window.addEventListener("react-os-shell:layout-mode-changed", refresh);
2251
+ return () => window.removeEventListener("react-os-shell:layout-mode-changed", refresh);
2252
+ }, []);
2253
+ const alwaysMaximized = alwaysMaximizedRaw && !widget;
2254
+ const [maximized, setMaximized] = useState(() => {
2255
+ if (boxKey && _windowPositions[boxKey] || initialBox) return alwaysMaximized;
2256
+ return alwaysMaximized || openMaximized;
2257
+ });
2258
+ const boxRef = useRef(box);
2259
+ boxRef.current = box;
2260
+ useLayoutEffect(() => {
2261
+ if (autoHeightResolved) return;
2262
+ const panel = panelRef.current;
2263
+ const body = bodyRef.current;
2264
+ if (!panel || !body) return;
2265
+ const floor = autoMinHeight ?? (widget ? 0 : 240);
2266
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
2267
+ let lastH = 0;
2268
+ let freezeTimer = null;
2269
+ let observed = null;
2270
+ const measure = () => {
2271
+ const content2 = body.firstElementChild;
2272
+ if (!content2 || panel.offsetHeight <= 0) return;
2273
+ if (content2 !== observed) {
2274
+ if (observed) ro.unobserve(observed);
2275
+ ro.observe(content2);
2276
+ observed = content2;
2277
+ }
2278
+ const chrome = panel.offsetHeight - body.offsetHeight;
2279
+ const prevFlex = body.style.flex, prevOverflow = body.style.overflowY, prevHeight = body.style.height;
2280
+ body.style.flex = "0 0 auto";
2281
+ body.style.height = "auto";
2282
+ body.style.overflowY = "visible";
2283
+ const bodyNatural = body.offsetHeight;
2284
+ const contentNatural = content2.offsetHeight;
2285
+ body.style.flex = prevFlex;
2286
+ body.style.height = prevHeight;
2287
+ body.style.overflowY = prevOverflow;
2288
+ const contentFilled = content2.offsetHeight;
2289
+ const fillsContainer = contentFilled > contentNatural + 4;
2290
+ const viewportCap = Math.max(floor, window.innerHeight - taskbarH - Math.max(0, boxRef.current.y) - 24);
2291
+ const target = fillsContainer ? Math.min(boxRef.current.h, viewportCap) : Math.min(Math.max(floor, chrome + bodyNatural), viewportCap);
2292
+ const changed = Math.abs(target - lastH) > 1;
2293
+ if (changed) {
2294
+ lastH = target;
2295
+ setBox((prev) => Math.abs(prev.h - target) > 1 ? { ...prev, h: target } : prev);
2296
+ }
2297
+ if (!widget) {
2298
+ if (target <= floor) {
2299
+ if (freezeTimer) {
2300
+ clearTimeout(freezeTimer);
2301
+ freezeTimer = null;
2302
+ }
2303
+ } else if (changed || freezeTimer === null) {
2304
+ if (freezeTimer) clearTimeout(freezeTimer);
2305
+ freezeTimer = setTimeout(() => {
2306
+ const before = lastH;
2307
+ measure();
2308
+ if (lastH === before) setAutoHeightResolved(true);
2309
+ }, 160);
2310
+ }
2311
+ }
2312
+ };
2313
+ const ro = new ResizeObserver(measure);
2314
+ measure();
2315
+ if (footerRef.current) ro.observe(footerRef.current);
2316
+ const mo = new MutationObserver(measure);
2317
+ mo.observe(body, { childList: true, subtree: true, characterData: true });
2318
+ return () => {
2319
+ ro.disconnect();
2320
+ mo.disconnect();
2321
+ if (freezeTimer) clearTimeout(freezeTimer);
2322
+ };
2323
+ }, [autoHeightResolved, widget, autoMinHeight, hasFooterContent]);
2324
+ useLayoutEffect(() => {
2325
+ if (!autoHeight || !autoHeightResolved || widget || !open) return;
2326
+ const footerEl = footerRef.current;
2327
+ if (!footerEl) return;
2328
+ let lastFooterH = footerEl.offsetHeight;
2329
+ const ro = new ResizeObserver(() => {
2330
+ if (!footerEl.isConnected) return;
2331
+ const h = footerEl.offsetHeight;
2332
+ const delta = h - lastFooterH;
2333
+ const flipped = h > 0 !== lastFooterH > 0;
2334
+ lastFooterH = h;
2335
+ if (!flipped || delta === 0 || maximized) return;
2336
+ if (document.body.classList.contains("rosh-gesturing")) return;
2337
+ const floor = autoMinHeight ?? 240;
2338
+ const taskbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--taskbar-height")) || 0;
2339
+ const viewportCap = Math.max(floor, window.innerHeight - taskbarH - Math.max(0, boxRef.current.y) - 24);
2340
+ setBox((prev) => ({ ...prev, h: Math.min(Math.max(floor, prev.h + delta), viewportCap) }));
2341
+ });
2342
+ ro.observe(footerEl);
2343
+ return () => ro.disconnect();
2344
+ }, [autoHeight, autoHeightResolved, widget, open, maximized, autoMinHeight]);
2345
+ const fitWidth = autoWidth !== false && !widget && !appStyle && !isMobile;
2346
+ const [autoWidthResolved, setAutoWidthResolved] = useState(() => !fitWidth);
2347
+ useLayoutEffect(() => {
2348
+ if (autoWidthResolved || !open) return;
2349
+ if (!fitWidth || maximized) {
2350
+ setAutoWidthResolved(true);
2351
+ return;
2352
+ }
2353
+ const panel = panelRef.current;
2354
+ const body = bodyRef.current;
2355
+ if (!panel || !body) return;
2356
+ const rigid = /* @__PURE__ */ new WeakSet();
2357
+ let expectedW = boxRef.current.w;
2358
+ let frame = 0;
2359
+ const stop = () => setAutoWidthResolved(true);
2360
+ const measure = () => {
2361
+ frame = 0;
2362
+ const cur = boxRef.current;
2363
+ if (Math.abs(cur.w - expectedW) > 1 || document.body.classList.contains("rosh-gesturing")) {
2364
+ stop();
2365
+ return;
2366
+ }
2367
+ const area = computeMaximizedBox();
2368
+ const capW = area.w - padding * 2;
2369
+ if (cur.w >= capW) {
2370
+ stop();
2371
+ return;
2372
+ }
2373
+ const scrollers = [];
2374
+ const over = [];
2375
+ for (const el of [body, ...Array.from(body.querySelectorAll("*"))]) {
2376
+ const o = el.scrollWidth - el.clientWidth;
2377
+ if (o <= 4 || rigid.has(el)) continue;
2378
+ const ox = getComputedStyle(el).overflowX;
2379
+ if (ox !== "auto" && ox !== "scroll") continue;
2380
+ scrollers.push(el);
2381
+ over.push(o);
2382
+ }
2383
+ if (!scrollers.length) return;
2384
+ const probeW = Math.min(capW, cur.w + Math.max(...over));
2385
+ const gain = probeW - cur.w;
2386
+ const prevWidth = panel.style.width;
2387
+ panel.style.width = `${probeW}px`;
2388
+ const measured = scrollers.map((el, i) => ({ over: over[i], residual: el.scrollWidth - el.clientWidth }));
2389
+ panel.style.width = prevWidth;
2390
+ measured.forEach((m, i) => {
2391
+ if (!scrollerIsFluid(m, gain)) rigid.add(scrollers[i]);
2392
+ });
2393
+ const next = growBoxToWidth(cur, widthToFit(cur.w, gain, measured), area, padding);
2394
+ if (next === cur) return;
2395
+ expectedW = next.w;
2396
+ setBox(next);
2397
+ };
2398
+ const schedule = () => {
2399
+ if (!frame) frame = requestAnimationFrame(measure);
2400
+ };
2401
+ measure();
2402
+ const mo = new MutationObserver(schedule);
2403
+ mo.observe(body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["style", "class", "hidden"] });
2404
+ const deadline = setTimeout(stop, AUTO_WIDTH_WATCH_MS);
2405
+ return () => {
2406
+ mo.disconnect();
2407
+ if (frame) cancelAnimationFrame(frame);
2408
+ clearTimeout(deadline);
2409
+ };
2410
+ }, [autoWidthResolved, open, fitWidth, maximized]);
2411
+ useEffect(() => {
2412
+ if (alwaysMaximized) {
2413
+ setMaximized(true);
2414
+ setBox(calcMaximized());
2415
+ }
2416
+ }, [alwaysMaximized, calcMaximized]);
2417
+ useEffect(() => {
2418
+ if (widget) return;
2419
+ const restore = () => {
2420
+ setMaximized(false);
2421
+ setBox(calcWindowed());
2422
+ };
2423
+ window.addEventListener("react-os-shell:restore-windowed", restore);
2424
+ return () => window.removeEventListener("react-os-shell:restore-windowed", restore);
2425
+ }, [widget, calcWindowed]);
2426
+ useEffect(() => {
2427
+ if (open && boxKey) {
2428
+ _windowPositions[boxKey] = box;
2429
+ _savePositionsDebounced();
2430
+ }
2431
+ if (open) window.dispatchEvent(new CustomEvent("modal-geometry"));
2432
+ }, [box, open, boxKey]);
2433
+ useEffect(() => {
2434
+ if (!open) return;
2435
+ const sync = () => {
2436
+ if (maximized) {
2437
+ setBox(calcMaximized());
2438
+ return;
2439
+ }
2440
+ if (document.body.classList.contains("rosh-gesturing")) return;
2441
+ setBox(clampReachable);
2442
+ };
2443
+ window.addEventListener("resize", sync);
2444
+ let observer = null;
2445
+ if (maximized) {
2446
+ observer = new MutationObserver(sync);
2447
+ observer.observe(document.body, { childList: true, subtree: false, attributes: true, attributeFilter: ["class"] });
2448
+ }
2449
+ return () => {
2450
+ window.removeEventListener("resize", sync);
2451
+ observer?.disconnect();
2452
+ };
2453
+ }, [open, maximized, calcMaximized]);
2454
+ useEffect(() => {
2455
+ if (!open) return;
2456
+ setTouched(false);
2457
+ touchedRef.current = false;
2458
+ closingRef.current = false;
2459
+ setAutoWidthResolved(!fitWidth);
2460
+ if (boxKey && _windowPositions[boxKey]) {
2461
+ const saved = _windowPositions[boxKey];
2462
+ setBox((prev) => clampReachable({
2463
+ ...saved,
2464
+ ...autoHeight ? { h: prev.h } : {},
2465
+ ...fitWidth ? { x: prev.x, w: prev.w } : {}
2466
+ }));
2467
+ return;
2468
+ }
2469
+ if (initialBox) {
2470
+ setBox({ x: initialBox.x, y: initialBox.y, w: initialBox.w, h: initialBox.h });
2471
+ setMaximized(initialBox.maximized);
2472
+ }
2473
+ }, [open]);
2474
+ useEffect(() => {
2475
+ if (!open || dirty !== "auto") return;
2476
+ let timer;
2477
+ const handler = (e) => {
2478
+ if (!panelRef.current?.contains(e.target) || touchedRef.current) return;
2479
+ touchedRef.current = true;
2480
+ timer = setTimeout(() => setTouched(true), 0);
2481
+ };
2482
+ document.addEventListener("input", handler, true);
2483
+ document.addEventListener("change", handler, true);
2484
+ return () => {
2485
+ clearTimeout(timer);
2486
+ document.removeEventListener("input", handler, true);
2487
+ document.removeEventListener("change", handler, true);
2488
+ };
2489
+ }, [open, dirty]);
2490
+ const isDirty = dirty === "auto" ? touched : dirty === true;
2491
+ useWindowDirty(!windowKey && open ? isDirty : false);
2492
+ const guardedClose = useCallback(async () => {
2493
+ if (closingRef.current) return;
2494
+ if (isDirty || dirty === "auto" && touchedRef.current) {
2495
+ closingRef.current = true;
2496
+ const name = titleTooltip;
2497
+ const ok = await confirm({
2498
+ title: "Discard changes?",
2499
+ message: name ? `\u201C${name}\u201D has unsaved changes. Are you sure you want to close? All changes will be lost.` : "You have unsaved changes. Are you sure you want to close? All changes will be lost.",
2500
+ confirmLabel: "Discard",
2501
+ cancelLabel: "Keep Editing",
2502
+ variant: "warning"
2503
+ });
2504
+ closingRef.current = false;
2505
+ if (!ok) return;
2506
+ }
2507
+ onClose();
2508
+ }, [isDirty, dirty, onClose, titleTooltip]);
2509
+ useLayoutEffect(() => {
2510
+ if (!open || !windowKey) return;
2511
+ const handler = (event) => {
2512
+ if (event.detail?.windowKey === windowKey) guardedClose();
2513
+ };
2514
+ window.addEventListener(MODAL_CLOSE_REQUEST_EVENT, handler);
2515
+ return () => window.removeEventListener(MODAL_CLOSE_REQUEST_EVENT, handler);
2516
+ }, [open, windowKey, guardedClose]);
2517
+ const preSnapBoxRef = useRef(null);
2518
+ const startDrag = useCallback((e) => {
2519
+ if (e.button !== 0) return;
2520
+ if (e.target.closest("button, input, a, kbd, select, textarea")) return;
2521
+ setWindowMenu(null);
2522
+ activateModal(modalId);
2523
+ if (alwaysMaximized) {
2524
+ e.preventDefault();
2525
+ return;
2526
+ }
2527
+ e.preventDefault();
2528
+ const dragHandle = e.currentTarget;
2529
+ const dragPointerId = e.pointerId;
2530
+ let endGesture = null;
2531
+ const sx = e.clientX, sy = e.clientY;
2532
+ const panel = panelRef.current;
2533
+ const rect = panel?.getBoundingClientRect();
2534
+ let ox, oy, actualH, actualW;
2535
+ if (preSnapBoxRef.current) {
2536
+ const restore = preSnapBoxRef.current;
2537
+ preSnapBoxRef.current = null;
2538
+ actualW = restore.w;
2539
+ actualH = restore.h;
2540
+ const offsetX = rect ? Math.min(Math.max(20, e.clientX - rect.left), restore.w - 20) : restore.w / 2;
2541
+ ox = e.clientX - offsetX;
2542
+ oy = e.clientY - 12;
2543
+ if (panel) {
2544
+ panel.style.width = `${actualW}px`;
2545
+ panel.style.height = `${actualH}px`;
2546
+ panel.style.left = `${ox}px`;
2547
+ panel.style.top = `${oy}px`;
2548
+ }
2549
+ } else {
2550
+ ox = rect ? rect.left : boxRef.current.x;
2551
+ oy = rect ? rect.top : boxRef.current.y;
2552
+ actualH = rect ? rect.height : boxRef.current.h;
2553
+ actualW = rect ? rect.width : boxRef.current.w;
2554
+ }
2555
+ setMaximized(false);
2556
+ setBox((b) => ({ ...b, x: ox, y: oy, w: actualW, h: actualH }));
2557
+ let currentZone = null;
2558
+ const move = (ev) => {
2559
+ if (!endGesture) {
2560
+ if (Math.abs(ev.clientX - sx) < DRAG_THRESHOLD && Math.abs(ev.clientY - sy) < DRAG_THRESHOLD) return;
2561
+ endGesture = beginPointerGesture(dragHandle, dragPointerId, "move", "move");
2562
+ if (panel) panel.style.transition = "none";
2563
+ }
2564
+ const nx = ox + ev.clientX - sx;
2565
+ const ny = Math.max(0, oy + ev.clientY - sy);
2566
+ if (panel) panel.style.transform = `translate(${nx - ox}px, ${ny - oy}px)`;
2567
+ boxRef.current = { ...boxRef.current, x: nx, y: ny };
2568
+ if (!widget) {
2569
+ const zone = detectSnapZone(ev.clientX, ev.clientY);
2570
+ if (zone !== currentZone) {
2571
+ currentZone = zone;
2572
+ if (zone) showSnapPreview(calcSnapBox(zone));
2573
+ else hideSnapPreview();
2574
+ }
2575
+ }
2576
+ };
2577
+ const up = () => {
2578
+ window.removeEventListener("pointermove", move);
2579
+ window.removeEventListener("pointerup", up);
2580
+ endGesture?.();
2581
+ hideSnapPreview();
2582
+ const finalBox = clampReachable({ ...boxRef.current });
2583
+ boxRef.current = finalBox;
2584
+ if (!widget && currentZone) {
2585
+ preSnapBoxRef.current = { x: ox, y: oy, w: actualW, h: actualH };
2586
+ const snapped = calcSnapBox(currentZone);
2587
+ if (panel) {
2588
+ panel.style.left = `${snapped.x}px`;
2589
+ panel.style.top = `${snapped.y}px`;
2590
+ panel.style.width = `${snapped.w}px`;
2591
+ panel.style.height = `${snapped.h}px`;
2592
+ panel.style.transform = "";
2593
+ panel.style.transition = "";
2594
+ }
2595
+ setBox(snapped);
2596
+ return;
2597
+ }
2598
+ if (widget) {
2599
+ const centerX = finalBox.x + finalBox.w / 2;
2600
+ const mid = window.innerWidth / 2;
2601
+ setWidgetAnchor(centerX > mid ? "right" : "left");
2602
+ }
2603
+ if (panel) {
2604
+ panel.style.left = `${finalBox.x}px`;
2605
+ panel.style.top = `${finalBox.y}px`;
2606
+ panel.style.transform = "";
2607
+ panel.style.transition = "";
2608
+ }
2609
+ setBox(finalBox);
2610
+ };
2611
+ window.addEventListener("pointermove", move);
2612
+ window.addEventListener("pointerup", up);
2613
+ }, []);
2614
+ const startResizeCorner = useCallback((e, corner) => {
2615
+ if (e.button !== 0) return;
2616
+ e.preventDefault();
2617
+ e.stopPropagation();
2618
+ const resizeHandle = e.currentTarget;
2619
+ const resizePointerId = e.pointerId;
2620
+ let endGesture = null;
2621
+ const sx = e.clientX, sy = e.clientY;
2622
+ const panel = panelRef.current;
2623
+ const rect = panel?.getBoundingClientRect();
2624
+ const ox = rect ? rect.left : boxRef.current.x;
2625
+ const oy = rect ? rect.top : boxRef.current.y;
2626
+ const ow = rect ? rect.width : boxRef.current.w;
2627
+ const oh = rect ? rect.height : boxRef.current.h;
2628
+ const isWest = corner === "sw" || corner === "nw" || corner === "w";
2629
+ const isNorth = corner === "ne" || corner === "nw" || corner === "n";
2630
+ const isEast = corner === "se" || corner === "ne" || corner === "e";
2631
+ const isSouth = corner === "se" || corner === "sw" || corner === "s";
2632
+ setMaximized(false);
2633
+ setBox({ x: ox, y: oy, w: ow, h: oh });
2634
+ boxRef.current = { x: ox, y: oy, w: ow, h: oh };
2635
+ const compute = (dx, dy) => {
2636
+ let nx = ox, ny = oy, nw = ow, nh = oh;
2637
+ if (isEast) nw = Math.max(MIN_W, ow + dx);
2638
+ if (isSouth) nh = Math.max(MIN_H, oh + dy);
2639
+ if (isWest) {
2640
+ const targetW = Math.max(MIN_W, ow - dx);
2641
+ nx = ox + ow - targetW;
2642
+ nw = targetW;
2643
+ }
2644
+ if (isNorth) {
2645
+ const targetH = Math.max(MIN_H, oh - dy);
2646
+ ny = oy + oh - targetH;
2647
+ nh = targetH;
2648
+ }
2649
+ return { x: nx, y: ny, w: nw, h: nh };
2650
+ };
2651
+ let raf = 0;
2652
+ let pending = null;
2653
+ const flush = () => {
2654
+ raf = 0;
2655
+ if (!pending) return;
2656
+ const next = pending;
2657
+ pending = null;
2658
+ boxRef.current = next;
2659
+ if (panel) {
2660
+ panel.style.left = `${next.x}px`;
2661
+ panel.style.top = `${next.y}px`;
2662
+ panel.style.width = `${next.w}px`;
2663
+ panel.style.height = `${next.h}px`;
2664
+ }
2665
+ };
2666
+ const move = (ev) => {
2667
+ if (!endGesture) {
2668
+ if (Math.abs(ev.clientX - sx) < DRAG_THRESHOLD && Math.abs(ev.clientY - sy) < DRAG_THRESHOLD) return;
2669
+ endGesture = beginPointerGesture(resizeHandle, resizePointerId, RESIZE_CURSOR[corner], "resize");
2670
+ }
2671
+ pending = compute(ev.clientX - sx, ev.clientY - sy);
2672
+ if (!raf) raf = requestAnimationFrame(flush);
2673
+ };
2674
+ const up = () => {
2675
+ window.removeEventListener("pointermove", move);
2676
+ window.removeEventListener("pointerup", up);
2677
+ if (raf) cancelAnimationFrame(raf);
2678
+ flush();
2679
+ endGesture?.();
2680
+ setBox({ ...boxRef.current });
2681
+ };
2682
+ window.addEventListener("pointermove", move);
2683
+ window.addEventListener("pointerup", up);
2684
+ }, []);
2685
+ const KEY_STEP = 24;
2686
+ const onTitleBarKeyDown = useCallback((e) => {
2687
+ if (e.target.closest("button, input, a, kbd, select, textarea")) return;
2688
+ const arrows = {
2689
+ ArrowLeft: [-1, 0],
2690
+ ArrowRight: [1, 0],
2691
+ ArrowUp: [0, -1],
2692
+ ArrowDown: [0, 1]
2693
+ };
2694
+ const dir = arrows[e.key];
2695
+ if (!dir && e.key !== "Enter") return;
2696
+ e.preventDefault();
2697
+ e.stopPropagation();
2698
+ activateModal(modalId);
2699
+ if (e.key === "Enter") {
2700
+ if (maximized) {
2701
+ setMaximized(false);
2702
+ setBox(calcWindowed());
2703
+ } else {
2704
+ setMaximized(true);
2705
+ setBox(calcMaximized());
2706
+ }
2707
+ return;
2708
+ }
2709
+ const [dx, dy] = dir;
2710
+ if (e.ctrlKey || e.metaKey) {
2711
+ const savePreSnap = () => {
2712
+ if (!preSnapBoxRef.current) preSnapBoxRef.current = { ...boxRef.current };
2713
+ };
2714
+ if (dy < 0) {
2715
+ savePreSnap();
2716
+ setMaximized(true);
2717
+ setBox(calcMaximized());
2718
+ return;
2719
+ }
2720
+ if (dy > 0) {
2721
+ if (maximized) {
2722
+ setMaximized(false);
2723
+ setBox(calcWindowed());
2724
+ return;
2725
+ }
2726
+ if (preSnapBoxRef.current) {
2727
+ const restore = clampReachable(preSnapBoxRef.current);
2728
+ preSnapBoxRef.current = null;
2729
+ boxRef.current = restore;
2730
+ setBox(restore);
2731
+ return;
2732
+ }
2733
+ _minimizeModal(modalId);
2734
+ return;
2735
+ }
2736
+ savePreSnap();
2737
+ setMaximized(false);
2738
+ const snapped = calcSnapBox(dx < 0 ? "left" : "right");
2739
+ boxRef.current = snapped;
2740
+ setBox(snapped);
2741
+ return;
2742
+ }
2743
+ if (maximized) return;
2744
+ const b = boxRef.current;
2745
+ const next = e.shiftKey ? { ...b, w: Math.max(MIN_W, b.w + dx * KEY_STEP), h: Math.max(MIN_H, b.h + dy * KEY_STEP) } : clampReachable({ ...b, x: b.x + dx * KEY_STEP, y: Math.max(0, b.y + dy * KEY_STEP) });
2746
+ boxRef.current = next;
2747
+ setBox(next);
2748
+ }, [modalId, maximized, calcWindowed, calcMaximized]);
2749
+ const applySnapZone = useCallback((zone) => {
2750
+ if (!preSnapBoxRef.current) preSnapBoxRef.current = { ...boxRef.current };
2751
+ setMaximized(false);
2752
+ const snapped = calcSnapBox(zone);
2753
+ boxRef.current = snapped;
2754
+ setBox(snapped);
2755
+ activateModal(modalId);
2756
+ }, [modalId]);
2757
+ const titleBarKeyProps = alwaysMaximized || exposeActive ? {} : {
2758
+ tabIndex: 0,
2759
+ role: "group",
2760
+ "aria-label": titleTooltip ? `${titleTooltip} \u2014 ${winStr.windowSuffix}` : winStr.windowSuffix,
2761
+ "aria-keyshortcuts": "ArrowUp ArrowDown ArrowLeft ArrowRight Shift+ArrowUp Shift+ArrowDown Shift+ArrowLeft Shift+ArrowRight Control+ArrowUp Control+ArrowDown Control+ArrowLeft Control+ArrowRight Enter",
2762
+ onKeyDown: onTitleBarKeyDown
2763
+ };
2764
+ const reset = () => {
2765
+ setMaximized(true);
2766
+ setBox(calcMaximized());
2767
+ };
2768
+ const submitModal = useCallback(() => {
2769
+ const panel = panelRef.current;
2770
+ if (!panel) return;
2771
+ const form = panel.querySelector("form");
2772
+ if (form) {
2773
+ form.requestSubmit();
2774
+ } else {
2775
+ const btn = panel.querySelector(MODAL_SUBMIT_SELECTOR);
2776
+ if (btn && !btn.disabled) btn.click();
2777
+ }
2778
+ }, []);
2779
+ useEffect(() => {
2780
+ if (!open) return;
2781
+ const handler = (e) => {
2782
+ if (activationOrder[activationOrder.length - 1] !== modalId) return;
2783
+ if (e.key === "Enter") {
2784
+ const target = e.target;
2785
+ if (target.tagName === "TEXTAREA" || target.isContentEditable) return;
2786
+ if (e.metaKey || e.ctrlKey) {
2787
+ e.preventDefault();
2788
+ submitModal();
2789
+ } else if (target.tagName === "INPUT" && target.closest("form")) {
2790
+ return;
2791
+ } else {
2792
+ e.preventDefault();
2793
+ }
2794
+ }
2795
+ };
2796
+ window.addEventListener("keydown", handler, true);
2797
+ return () => window.removeEventListener("keydown", handler, true);
2798
+ }, [open, submitModal, modalId]);
2799
+ useEffect(() => {
2800
+ if (!open) return;
2801
+ const handler = (e) => {
2802
+ if (activationOrder[activationOrder.length - 1] !== modalId) return;
2803
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
2804
+ e.preventDefault();
2805
+ document.dispatchEvent(new CustomEvent("modal-save", { detail: { modalId } }));
2806
+ touchedRef.current = false;
2807
+ setTouched(false);
2808
+ } else if (e.altKey && e.shiftKey && (e.code === "KeyD" || e.key === "D" || e.key === "d")) {
2809
+ e.preventDefault();
2810
+ document.dispatchEvent(new CustomEvent("modal-duplicate", { detail: { modalId } }));
2811
+ }
2812
+ };
2813
+ window.addEventListener("keydown", handler, true);
2814
+ return () => window.removeEventListener("keydown", handler, true);
2815
+ }, [open]);
2816
+ useEffect(() => {
2817
+ if (!open) return;
2818
+ if (widget) return;
2819
+ const handler = (e) => {
2820
+ if (e.key === "Escape" && activationOrder[activationOrder.length - 1] === modalId) {
2821
+ if (runEscapeInterceptors(e)) {
2822
+ e.preventDefault();
2823
+ e.stopPropagation();
2824
+ return;
2825
+ }
2826
+ e.preventDefault();
2827
+ e.stopPropagation();
2828
+ guardedClose();
2829
+ }
2830
+ };
2831
+ window.addEventListener("keydown", handler, true);
2832
+ return () => window.removeEventListener("keydown", handler, true);
2833
+ }, [open, guardedClose, modalId, widget]);
2834
+ const hasNav = open && (onNext !== void 0 || onPrev !== void 0);
2835
+ const showBoundaryToast = useCallback((edge) => {
2836
+ const existing = document.getElementById("nav-boundary-toast");
2837
+ if (existing) existing.remove();
2838
+ const lines = edge === "bottom" ? ["That's the last one, chief.", "You've hit rock bottom... of the list.", "End of the line, buddy.", "Nothing left. Go outside.", "Congratulations, you scrolled to the void.", "There's no more. I checked.", "You've reached the edge of the known universe.", "Plot twist: there is no next one.", "Even scrolling has limits. Unlike your ambition.", "Last one. Time to touch grass."] : ["Already at the top. Overachiever.", "There is no item above this. Trust me.", "You're #1. Literally.", "Can't go higher. You've peaked.", "This is it. The summit. The top.", "First item. No VIP lounge above this.", "Going up? Nope. Elevator's broken.", "You want negative indexes? Bold.", "Top of the list. Top of the world.", "The only way is down from here."];
2839
+ const msg = lines[Math.floor(Math.random() * lines.length)];
2840
+ const toast = document.createElement("div");
2841
+ toast.id = "nav-boundary-toast";
2842
+ toast.className = "fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] bg-gray-900 text-white px-5 py-2.5 rounded-lg shadow-lg text-sm font-medium transition-opacity duration-300";
2843
+ toast.textContent = msg;
2844
+ document.body.appendChild(toast);
2845
+ setTimeout(() => {
2846
+ toast.style.opacity = "0";
2847
+ setTimeout(() => toast.remove(), 300);
2848
+ }, 2e3);
2849
+ }, []);
2850
+ useEffect(() => {
2851
+ if (!open) return;
2852
+ const handler = (e) => {
2853
+ if (activationOrder[activationOrder.length - 1] !== modalId) return;
2854
+ const tag = e.target.tagName;
2855
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
2856
+ if (e.target.isContentEditable) return;
2857
+ if (e.key === "j" && hasNav) {
2858
+ e.preventDefault();
2859
+ if (onNext) onNext();
2860
+ else showBoundaryToast("bottom");
2861
+ }
2862
+ if (e.key === "k" && hasNav) {
2863
+ e.preventDefault();
2864
+ if (onPrev) onPrev();
2865
+ else showBoundaryToast("top");
2866
+ }
2867
+ };
2868
+ window.addEventListener("keydown", handler);
2869
+ return () => window.removeEventListener("keydown", handler);
2870
+ }, [open, onNext, onPrev, hasNav, showBoundaryToast, modalId]);
2871
+ if (!open || interceptedRef.current) return null;
2872
+ const exposeTile = exposeActive ? computeExposeTile(modalId) : null;
2873
+ const exposeScale = exposeActive && exposeTile ? Math.min(exposeTile.w / box.w, exposeTile.h / box.h) : 1;
2874
+ const exposeStyle = (() => {
2875
+ if (exposeActive && exposeTile) {
2876
+ const scale = exposeScale;
2877
+ const scaledW = box.w * scale;
2878
+ const scaledH = box.h * scale;
2879
+ const tx = exposeTile.x + (exposeTile.w - scaledW) / 2 - box.x;
2880
+ const ty = exposeTile.y + (exposeTile.h - scaledH) / 2 - box.y;
2881
+ const lit = exposeHovered || exposeKeyboardHighlight;
2882
+ return {
2883
+ transform: `translate(${tx}px, ${ty}px) scale(${scale})`,
2884
+ transformOrigin: "top left",
2885
+ transition: "transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 280ms",
2886
+ cursor: "pointer",
2887
+ // Lifted tile sits above its neighbours so the glow isn't clipped.
2888
+ zIndex: lit ? 2020 : 2010,
2889
+ boxShadow: lit ? "0 24px 72px rgba(0,0,0,0.6), 0 0 0 2px rgba(255,255,255,0.55), 0 0 36px 12px rgba(96,165,250,0.85), 0 0 96px 28px rgba(96,165,250,0.55)" : "0 16px 48px rgba(0,0,0,0.55)",
2890
+ pointerEvents: "auto"
2891
+ };
2892
+ }
2893
+ if (exposeExiting && isExposeTileable) {
2894
+ const picked = exposeExitRole === "picked";
2895
+ return {
2896
+ transition: picked ? "transform 640ms cubic-bezier(0.34, 1.42, 0.64, 1), box-shadow 600ms" : "transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 600ms",
2897
+ ...picked ? { zIndex: 2030 } : {}
2898
+ };
2899
+ }
2900
+ return null;
2901
+ })();
2902
+ const content = /* @__PURE__ */ jsxs("div", { children: [
2903
+ /* @__PURE__ */ jsxs(
2904
+ "div",
2905
+ {
2906
+ ref: panelRef,
2907
+ "data-modal-panel": true,
2908
+ "data-modal-id": modalId,
2909
+ "data-window-key": windowKey || void 0,
2910
+ ...allowPinOnTop ? { "data-utility": "" } : {},
2911
+ ...pinnedOnTop ? { "data-pinned-top": "" } : {},
2912
+ ...widget ? { "data-widget": "" } : {},
2913
+ className: `fixed rounded-2xl flex flex-col overflow-hidden ${widget ? isActive ? "shadow-2xl" : "shadow-lg" : `border ${isActive ? "shadow-2xl border-gray-200" : "shadow-lg border-gray-300"}`}`,
2914
+ onMouseDownCapture: (e) => {
2915
+ swallowInactiveClickRef.current = false;
2916
+ if (exposeActive) {
2917
+ if (e.target.closest("[data-expose-close]")) return;
2918
+ e.preventDefault();
2919
+ e.stopPropagation();
2920
+ setExposeExitFocus(modalId);
2921
+ setExposeState(false);
2922
+ activateModal(modalId);
2923
+ return;
2924
+ }
2925
+ if (!isActive && !widget && !pinnedOnTop && e.button === 0) {
2926
+ const target = e.target;
2927
+ if (target.closest("[data-modal-panel]") === panelRef.current && target.closest("button, input, a, select, textarea") && !target.closest("[data-window-chrome]")) {
2928
+ e.preventDefault();
2929
+ e.stopPropagation();
2930
+ swallowInactiveClickRef.current = true;
2931
+ setWindowMenu(null);
2932
+ activateModal(modalId);
2933
+ window.addEventListener("mouseup", () => {
2934
+ setTimeout(() => {
2935
+ swallowInactiveClickRef.current = false;
2936
+ }, 0);
2937
+ }, { once: true });
2938
+ }
2939
+ }
2940
+ },
2941
+ onClickCapture: (e) => {
2942
+ if (swallowInactiveClickRef.current) {
2943
+ swallowInactiveClickRef.current = false;
2944
+ e.preventDefault();
2945
+ e.stopPropagation();
2946
+ }
2947
+ },
2948
+ onMouseDown: (e) => {
2949
+ if (exposeActive) return;
2950
+ setWindowMenu(null);
2951
+ const targetPanel = e.target.closest("[data-modal-panel]");
2952
+ if (targetPanel !== panelRef.current) return;
2953
+ if (!e.target.closest("button, input, a, select, textarea")) {
2954
+ activateModal(modalId);
2955
+ }
2956
+ },
2957
+ style: isMobile ? {
2958
+ // Mobile fullscreen: ignore stored box, fill viewport down to bottom
2959
+ // nav. Widgets stay hidden on mobile. Only the active window is
2960
+ // visible — all others hidden so swipe-right reveals the home
2961
+ // wallpaper backdrop. Exception: when a sibling Modal is being
2962
+ // swiped to-back and we're its `openedFrom` parent, un-hide so the
2963
+ // user sees their parent list during the slide instead of just the
2964
+ // wallpaper.
2965
+ zIndex: zIndex + 1,
2966
+ top: 0,
2967
+ left: 0,
2968
+ right: 0,
2969
+ bottom: "var(--mobile-bottom-nav, 56px)",
2970
+ width: "auto",
2971
+ height: "auto",
2972
+ transform: `translateX(${swipeX}px)`,
2973
+ transition: swipeDragging ? "none" : "transform 180ms ease-out",
2974
+ ...widget ? { display: "none" } : {},
2975
+ ...zIndex < 0 ? { display: "none" } : {},
2976
+ ...!isActive && !pinnedOnTop && !(swipingParentKey && windowKey === swipingParentKey) ? { display: "none" } : {}
2977
+ } : {
2978
+ // The panel is always a definite height — even while an `autoHeight`
2979
+ // window is still measuring (box.h is seeded from the size ladder).
2980
+ // The measurement effect adjusts box.h to hug the content; rendering
2981
+ // at a definite height the whole way through means fill-height
2982
+ // content (flex-1 / h-full) never collapses to the floor.
2983
+ zIndex: pinnedOnTop ? Z_LAYERS.pinnedWindow : zIndex + 1,
2984
+ width: box.w,
2985
+ height: box.h,
2986
+ top: box.y,
2987
+ ...widget && widgetAnchor === "right" ? { right: window.innerWidth - box.x - box.w } : { left: box.x },
2988
+ ...zIndex < 0 && !pinnedOnTop ? { display: "none" } : {},
2989
+ ...exposeStyle ?? {}
2990
+ },
2991
+ children: [
2992
+ isMobile && !widget && /* @__PURE__ */ jsx(
2993
+ "div",
2994
+ {
2995
+ onPointerDown: handleEdgePointerDown,
2996
+ className: "absolute top-0 bottom-0 left-0 w-[22px] z-[5]",
2997
+ style: { touchAction: "pan-y" },
2998
+ "aria-hidden": "true"
2999
+ }
3000
+ ),
3001
+ widget ? (
3002
+ /* Widget: no title bar — drag via body, close via right-click context menu */
3003
+ null
3004
+ ) : isMobile ? null : compact ? (
3005
+ /* Compact: smaller title bar with title + close only */
3006
+ /* @__PURE__ */ jsxs(
3007
+ "div",
3008
+ {
3009
+ onPointerDown: startDrag,
3010
+ "data-window-chrome": "",
3011
+ ...titleBarKeyProps,
3012
+ className: `flex items-center justify-between px-3 py-1.5 border-b border-gray-200 shrink-0 cursor-move select-none rounded-t-2xl focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-400 ${isActive ? "backdrop-blur-sm" : ""}`,
3013
+ style: { touchAction: "none", backgroundColor: isActive ? `rgb(var(--window-header-rgb) / var(--active-header-opacity, 0.8))` : `rgb(var(--window-header-rgb) / var(--inactive-header-opacity, 0.7))` },
3014
+ children: [
3015
+ /* @__PURE__ */ jsxs("div", { "data-window-title": true, title: titleTooltip, className: "text-sm font-medium min-w-0 flex-1 truncate flex items-center gap-1.5", style: { color: isActive ? "var(--window-title-active, rgb(17 24 39))" : "var(--window-title-inactive, rgb(156 163 175))" }, children: [
3016
+ !exposeActive && renderIconButton(),
3017
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3018
+ ] }),
3019
+ !exposeActive && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0 ml-2", children: [
3020
+ allowPinOnTop && /* @__PURE__ */ jsx(
3021
+ "button",
3022
+ {
3023
+ onClick: () => setPinnedOnTop((p) => !p),
3024
+ title: pinnedOnTop ? winStr.unpin : winStr.pinOnTop,
3025
+ className: `p-0.5 rounded hover:bg-gray-200 ${pinnedOnTop ? "text-blue-600" : "text-gray-400 hover:text-gray-600"}`,
3026
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: pinnedOnTop ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" }) })
3027
+ }
3028
+ ),
3029
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: guardedClose, className: "rounded p-0.5 text-gray-400 hover:text-gray-600 hover:bg-gray-200", children: /* @__PURE__ */ jsx(XMarkIcon, { className: "h-4 w-4" }) })
3030
+ ] })
3031
+ ]
3032
+ }
3033
+ )
3034
+ ) : appStyle ? (
3035
+ /* App style: small title bar like compact, but keeps minimize/maximize for full window control. */
3036
+ /* @__PURE__ */ jsxs(
3037
+ "div",
3038
+ {
3039
+ onPointerDown: startDrag,
3040
+ "data-window-chrome": "",
3041
+ ...titleBarKeyProps,
3042
+ className: `flex items-center justify-between px-3 py-1.5 border-b border-gray-200 shrink-0 cursor-move select-none rounded-t-2xl focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-400 ${isActive ? "backdrop-blur-sm" : ""}`,
3043
+ style: { touchAction: "none", backgroundColor: isActive ? `rgb(var(--window-header-rgb) / var(--active-header-opacity, 0.8))` : `rgb(var(--window-header-rgb) / var(--inactive-header-opacity, 0.7))` },
3044
+ children: [
3045
+ /* @__PURE__ */ jsxs("div", { "data-window-title": true, title: titleTooltip, className: "text-sm font-medium min-w-0 flex-1 truncate flex items-center gap-1.5", style: { color: isActive ? "var(--window-title-active, rgb(17 24 39))" : "var(--window-title-inactive, rgb(156 163 175))" }, children: [
3046
+ !exposeActive && renderIconButton(),
3047
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3048
+ ] }),
3049
+ !exposeActive && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-2", children: [
3050
+ allowPinOnTop && /* @__PURE__ */ jsx(
3051
+ "button",
3052
+ {
3053
+ onClick: () => setPinnedOnTop((p) => !p),
3054
+ title: pinnedOnTop ? winStr.unpin : winStr.pinOnTop,
3055
+ className: `p-0.5 rounded hover:bg-gray-200 ${pinnedOnTop ? "text-blue-600" : "text-gray-400 hover:text-gray-600"}`,
3056
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: pinnedOnTop ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" }) })
3057
+ }
3058
+ ),
3059
+ /* @__PURE__ */ jsx("button", { onClick: () => _minimizeModal(modalId), title: winStr.minimize, className: "text-gray-400 hover:text-gray-600 px-1 py-0.5 rounded hover:bg-gray-200 text-xs leading-none", children: "\u2500" }),
3060
+ !alwaysMaximized && /* @__PURE__ */ jsx(
3061
+ SnapLayoutsButton,
3062
+ {
3063
+ maximized,
3064
+ onToggleMax: () => {
3065
+ if (maximized) {
3066
+ setMaximized(false);
3067
+ setBox(calcWindowed());
3068
+ } else {
3069
+ reset();
3070
+ }
3071
+ },
3072
+ onSnap: applySnapZone,
3073
+ buttonClassName: "text-gray-400 hover:text-gray-600 px-1 py-0.5 rounded hover:bg-gray-200 text-xs leading-none",
3074
+ strings: winStr
3075
+ }
3076
+ ),
3077
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: guardedClose, className: "rounded p-0.5 text-gray-400 hover:text-gray-600 hover:bg-gray-200", children: /* @__PURE__ */ jsx(XMarkIcon, { className: "h-4 w-4" }) })
3078
+ ] })
3079
+ ]
3080
+ }
3081
+ )
3082
+ ) : /* @__PURE__ */ jsxs(
3083
+ "div",
3084
+ {
3085
+ onPointerDown: startDrag,
3086
+ "data-window-chrome": "",
3087
+ ...titleBarKeyProps,
3088
+ className: `flex items-center justify-between px-4 py-2.5 border-b border-gray-200 shrink-0 cursor-move select-none rounded-t-2xl focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-400 ${isActive ? "backdrop-blur-sm" : ""}`,
3089
+ style: { touchAction: "none", backgroundColor: isActive ? `rgb(var(--window-header-rgb) / var(--active-header-opacity, 0.8))` : `rgb(var(--window-header-rgb) / var(--inactive-header-opacity, 0.7))` },
3090
+ children: [
3091
+ /* @__PURE__ */ jsxs("div", { "data-window-title": true, title: titleTooltip, className: "text-base font-medium min-w-0 flex-1 truncate flex items-center gap-2", style: { color: isActive ? "var(--window-title-active, rgb(17 24 39))" : "var(--window-title-inactive, rgb(156 163 175))" }, children: [
3092
+ !exposeActive && renderIconButton(),
3093
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3094
+ ] }),
3095
+ !exposeActive && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 shrink-0 ml-4", children: [
3096
+ hasNav && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 mr-1 text-[10px] text-gray-400", children: [
3097
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 font-medium text-gray-500", children: "K" }),
3098
+ /* @__PURE__ */ jsx("span", { children: winStr.prev }),
3099
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 font-medium ml-1 text-gray-500", children: "J" }),
3100
+ /* @__PURE__ */ jsx("span", { children: winStr.next })
3101
+ ] }),
3102
+ allowPinOnTop && /* @__PURE__ */ jsx(
3103
+ "button",
3104
+ {
3105
+ onClick: () => setPinnedOnTop((p) => !p),
3106
+ title: pinnedOnTop ? winStr.unpin : winStr.pinOnTop,
3107
+ className: `text-xs px-2 py-1 rounded hover:bg-gray-200 ${pinnedOnTop ? "text-blue-600" : "text-gray-400 hover:text-gray-600"}`,
3108
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: pinnedOnTop ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" }) })
3109
+ }
3110
+ ),
3111
+ /* @__PURE__ */ jsx("button", { onClick: () => _minimizeModal(modalId), title: winStr.minimize, className: "text-gray-400 hover:text-gray-600 text-xs px-2 py-1 rounded hover:bg-gray-200", children: "\u2500" }),
3112
+ !alwaysMaximized && /* @__PURE__ */ jsx(
3113
+ SnapLayoutsButton,
3114
+ {
3115
+ maximized,
3116
+ onToggleMax: () => {
3117
+ if (maximized) {
3118
+ setMaximized(false);
3119
+ setBox(calcWindowed());
3120
+ } else {
3121
+ reset();
3122
+ }
3123
+ },
3124
+ onSnap: applySnapZone,
3125
+ buttonClassName: "text-gray-400 hover:text-gray-600 text-xs px-2 py-1 rounded hover:bg-gray-200",
3126
+ strings: winStr
3127
+ }
3128
+ ),
3129
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 text-[10px] font-medium text-gray-400", children: "ESC" }),
3130
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: guardedClose, className: "rounded-md text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx(XMarkIcon, { className: "h-5 w-5" }) })
3131
+ ] })
3132
+ ]
3133
+ }
3134
+ ),
3135
+ accentRgb && accentStripeOn && !widget && !isMobile && /* @__PURE__ */ jsx(
3136
+ "div",
3137
+ {
3138
+ "aria-hidden": "true",
3139
+ className: "h-[3px] shrink-0 pointer-events-none",
3140
+ style: { backgroundColor: `rgb(var(--window-accent-rgb) / ${isActive ? 0.9 : 0.55})` }
3141
+ }
3142
+ ),
3143
+ /* @__PURE__ */ jsx(ModalIdContext.Provider, { value: modalId, children: /* @__PURE__ */ jsx(ModalActionsContext.Provider, { value: { rightRef: actionsRef, leftRef: actionsLeftRef, notify: () => setHasActions(true), active: isActive, isDirty, footerOpen: hasFooterContent && !widget && !compact && !appStyle && !isMobile }, children: /* @__PURE__ */ jsx(
3144
+ "div",
3145
+ {
3146
+ ref: bodyRef,
3147
+ ...widget ? { onPointerDown: startDrag, onContextMenu: (e) => {
3148
+ e.preventDefault();
3149
+ setCtxMenu({ x: e.clientX, y: e.clientY });
3150
+ } } : {},
3151
+ className: `flex-1 min-h-0 flex flex-col ${widget ? "p-0 cursor-move" : appStyle ? "p-0" : flushBody ? "p-0" : compact ? "p-2" : "p-4"} ${widget ? "" : "backdrop-blur-sm"} ${bodyScroll === false || appStyle || flushBody ? "overflow-hidden" : "overflow-y-auto overscroll-contain"} ${widget ? "rounded-2xl select-none" : ""}`,
3152
+ style: { ...widget ? { touchAction: "none" } : {}, backgroundColor: widget ? "transparent" : isActive ? `rgb(var(--window-content-rgb) / var(--active-content-opacity, 0.9))` : `rgb(var(--window-content-rgb) / var(--inactive-content-opacity, 0.8))` },
3153
+ children: /* @__PURE__ */ jsx(WindowErrorBoundary, { children })
3154
+ }
3155
+ ) }) }),
3156
+ widget && ctxMenu && /* @__PURE__ */ jsxs(PopupMenu, { minWidth: 160, style: { left: ctxMenu.x, top: ctxMenu.y }, onClose: () => setCtxMenu(null), children: [
3157
+ /* @__PURE__ */ jsx(PopupMenuItem, { onClick: () => {
3158
+ setCtxMenu(null);
3159
+ window.dispatchEvent(new CustomEvent("widget-open-settings", { detail: modalId }));
3160
+ }, children: "Settings" }),
3161
+ /* @__PURE__ */ jsx(PopupMenuDivider, {}),
3162
+ widgetMenu && /* @__PURE__ */ jsxs(Fragment, { children: [
3163
+ widgetMenu,
3164
+ /* @__PURE__ */ jsx(PopupMenuDivider, {})
3165
+ ] }),
3166
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3167
+ setCtxMenu(null);
3168
+ setPinnedOnTop((p) => !p);
3169
+ }, children: [
3170
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: "Always on Top" }),
3171
+ pinnedOnTop && /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-blue-600 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) })
3172
+ ] }),
3173
+ /* @__PURE__ */ jsx(PopupMenuDivider, {}),
3174
+ /* @__PURE__ */ jsx(PopupMenuItem, { onClick: () => {
3175
+ setCtxMenu(null);
3176
+ guardedClose();
3177
+ }, children: "Close" })
3178
+ ] }),
3179
+ /* @__PURE__ */ jsxs(
3180
+ "div",
3181
+ {
3182
+ onPointerDown: startDrag,
3183
+ ref: footerRef,
3184
+ className: `px-4 py-2 border-t border-gray-200 shrink-0 flex items-center justify-between text-xs select-none cursor-move${isActive ? " backdrop-blur-sm" : ""}${widget || compact || appStyle || isMobile || !hasFooterContent ? " hidden" : ""}`,
3185
+ style: { touchAction: "none", backgroundColor: isActive ? `rgb(var(--window-footer-rgb) / var(--active-header-opacity, 0.8))` : `rgb(var(--window-footer-rgb) / var(--inactive-header-opacity, 0.7))` },
3186
+ children: [
3187
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
3188
+ showsUndo && /* @__PURE__ */ jsx(UndoControls, { auto: true }),
3189
+ actionsLeft,
3190
+ /* @__PURE__ */ jsx("div", { ref: actionsLeftRef, "data-modal-actions-left": true, className: "flex items-center gap-2" }),
3191
+ footer
3192
+ ] }),
3193
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 ml-auto", children: [
3194
+ /* @__PURE__ */ jsx("div", { ref: actionsRef, "data-modal-actions": true, className: "flex items-center gap-2" }),
3195
+ actions
3196
+ ] })
3197
+ ]
3198
+ }
3199
+ ),
3200
+ !widget && !isMobile && !exposeActive && isActive && /* @__PURE__ */ jsxs(Fragment, { children: [
3201
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "se"), className: "absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize z-10" }),
3202
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "sw"), className: "absolute bottom-0 left-0 w-3 h-3 cursor-nesw-resize z-10" }),
3203
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "ne"), className: "absolute top-0 right-0 w-3 h-3 cursor-nesw-resize z-10" }),
3204
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "nw"), className: "absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-10" }),
3205
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "n"), className: "absolute top-0 left-3 right-3 h-1 cursor-ns-resize" }),
3206
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "s"), className: "absolute bottom-0 left-3 right-3 h-1 cursor-ns-resize" }),
3207
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "e"), className: "absolute top-3 bottom-3 right-0 w-1 cursor-ew-resize" }),
3208
+ /* @__PURE__ */ jsx("div", { onPointerDown: (e) => startResizeCorner(e, "w"), className: "absolute top-3 bottom-3 left-0 w-1 cursor-ew-resize" })
3209
+ ] }),
3210
+ exposeActive && /* @__PURE__ */ jsx(
3211
+ "div",
3212
+ {
3213
+ className: "absolute inset-0",
3214
+ style: { zIndex: 9999, cursor: "pointer", background: "transparent" },
3215
+ onMouseEnter: () => setExposeHovered(true),
3216
+ onMouseLeave: () => setExposeHovered(false),
3217
+ onMouseDown: (e) => {
3218
+ e.stopPropagation();
3219
+ e.preventDefault();
3220
+ activateModal(modalId);
3221
+ setExposeState(false);
3222
+ },
3223
+ onClick: (e) => {
3224
+ e.stopPropagation();
3225
+ e.preventDefault();
3226
+ },
3227
+ children: exposeHovered && /* @__PURE__ */ jsx(
3228
+ "button",
3229
+ {
3230
+ type: "button",
3231
+ "data-expose-close": true,
3232
+ "aria-label": shellStrings.taskbar.closeWindow,
3233
+ title: "Close",
3234
+ className: "absolute flex items-center justify-center rounded-full bg-black/55 text-white shadow-lg ring-1 ring-white/40 transition-colors hover:bg-red-500",
3235
+ style: {
3236
+ top: 8,
3237
+ right: 8,
3238
+ width: 30,
3239
+ height: 30,
3240
+ transform: `scale(${1 / exposeScale})`,
3241
+ transformOrigin: "top right",
3242
+ zIndex: 1e4
3243
+ },
3244
+ onMouseDown: (e) => {
3245
+ e.stopPropagation();
3246
+ e.preventDefault();
3247
+ },
3248
+ onClick: (e) => {
3249
+ e.stopPropagation();
3250
+ e.preventDefault();
3251
+ guardedClose();
3252
+ },
3253
+ children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
3254
+ }
3255
+ )
3256
+ }
3257
+ )
3258
+ ]
3259
+ }
3260
+ ),
3261
+ exposeActive && exposeTile && /* @__PURE__ */ jsx(
3262
+ "div",
3263
+ {
3264
+ className: "fixed pointer-events-none select-none truncate text-center",
3265
+ style: {
3266
+ left: exposeTile.x,
3267
+ top: exposeTile.y + exposeTile.h + 4,
3268
+ width: exposeTile.w,
3269
+ zIndex: exposeHovered ? 2021 : 2011,
3270
+ color: "white",
3271
+ fontSize: 12,
3272
+ fontWeight: 500,
3273
+ textShadow: "0 1px 2px rgba(0,0,0,0.6)"
3274
+ },
3275
+ children: extractTitleText(displayTitle)
3276
+ }
3277
+ )
3278
+ ] });
3279
+ const windowMenuEl = windowMenu && /* @__PURE__ */ jsxs(PopupMenu, { style: { left: windowMenu.x, top: windowMenu.y }, onClose: () => setWindowMenu(null), minWidth: 160, children: [
3280
+ !widget && !compact && /* @__PURE__ */ jsxs(Fragment, { children: [
3281
+ /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3282
+ _minimizeModal(modalId);
3283
+ setWindowMenu(null);
3284
+ }, children: [
3285
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 12h-15" }) }),
3286
+ winStr.minimize
3287
+ ] }),
3288
+ !alwaysMaximized && (maximized ? /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3289
+ setMaximized(false);
3290
+ setBox(calcWindowed());
3291
+ setWindowMenu(null);
3292
+ }, children: [
3293
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 4.5v15m6-15v15M4.5 9h15M4.5 15h15" }) }),
3294
+ winStr.windowed
3295
+ ] }) : /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3296
+ reset();
3297
+ setWindowMenu(null);
3298
+ }, children: [
3299
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" }) }),
3300
+ winStr.maximize
3301
+ ] }))
3302
+ ] }),
3303
+ allowPinOnTop && /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3304
+ setPinnedOnTop((p) => !p);
3305
+ setWindowMenu(null);
3306
+ }, children: [
3307
+ /* @__PURE__ */ jsx("svg", { className: `h-4 w-4 ${pinnedOnTop ? "text-blue-600" : "text-gray-400"}`, fill: pinnedOnTop ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" }) }),
3308
+ pinnedOnTop ? winStr.unpin : winStr.pinOnTop
3309
+ ] }),
3310
+ shortcutSpec && /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3311
+ setWindowMenu(null);
3312
+ toggleDesktopShortcut();
3313
+ }, children: [
3314
+ /* @__PURE__ */ jsx("svg", { className: `h-4 w-4 ${isOnDesktop ? "text-yellow-500" : "text-gray-400"}`, fill: isOnDesktop ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" }) }),
3315
+ isOnDesktop ? shellStrings.taskbar.removeFromDesktop : shellStrings.taskbar.addToDesktop
3316
+ ] }),
3317
+ (_extraMenuItems[modalId] || []).length > 0 && /* @__PURE__ */ jsx(Fragment, { children: (_extraMenuItems[modalId] || []).map((item, i) => /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
3318
+ setWindowMenu(null);
3319
+ item.onClick();
3320
+ }, children: [
3321
+ item.icon,
3322
+ item.label
3323
+ ] }, i)) }),
3324
+ /* @__PURE__ */ jsx(PopupMenuDivider, {}),
3325
+ /* @__PURE__ */ jsxs(PopupMenuItem, { danger: true, onClick: () => {
3326
+ setWindowMenu(null);
3327
+ guardedClose();
3328
+ }, children: [
3329
+ /* @__PURE__ */ jsx(XMarkIcon, { className: "h-4 w-4" }),
3330
+ winStr.close
3331
+ ] })
3332
+ ] });
3333
+ const listedInTaskbar = !!windowKey && openWindows.some((w) => w.id === windowKey);
3334
+ const restoreTab = isMinimized && !listedInTaskbar && !widget ? /* @__PURE__ */ jsxs(
3335
+ "div",
3336
+ {
3337
+ "data-modal-restore-tab": true,
3338
+ "data-modal-id": modalId,
3339
+ className: "fixed left-1/2 z-[1200] flex -translate-x-1/2 items-center gap-1 rounded-lg border border-gray-300 bg-white px-1.5 py-1 shadow-lg",
3340
+ style: { bottom: "calc(var(--taskbar-height, 0px) + 12px)" },
3341
+ children: [
3342
+ /* @__PURE__ */ jsx(
3343
+ "button",
3344
+ {
3345
+ type: "button",
3346
+ onClick: () => activateModal(modalId),
3347
+ title: winStr.restore,
3348
+ className: "max-w-[16rem] truncate rounded px-2 py-0.5 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900",
3349
+ children: titleTooltip || winStr.windowSuffix
3350
+ }
3351
+ ),
3352
+ /* @__PURE__ */ jsx(
3353
+ "button",
3354
+ {
3355
+ type: "button",
3356
+ onClick: () => activateModal(modalId),
3357
+ title: winStr.restore,
3358
+ "aria-label": winStr.restore,
3359
+ className: "rounded px-1.5 py-0.5 text-xs leading-none text-gray-400 hover:bg-gray-100 hover:text-gray-600",
3360
+ children: "\u25B4"
3361
+ }
3362
+ ),
3363
+ /* @__PURE__ */ jsx(
3364
+ "button",
3365
+ {
3366
+ type: "button",
3367
+ onClick: () => {
3368
+ void guardedClose();
3369
+ },
3370
+ title: winStr.close,
3371
+ "aria-label": winStr.close,
3372
+ className: "rounded px-1.5 py-0.5 text-xs leading-none text-gray-400 hover:bg-gray-100 hover:text-gray-600",
3373
+ children: "\u2715"
3374
+ }
3375
+ )
3376
+ ]
3377
+ }
3378
+ ) : null;
3379
+ return createPortal(
3380
+ /* @__PURE__ */ jsxs(WindowShortcutContext.Provider, { value: null, children: [
3381
+ content,
3382
+ windowMenuEl,
3383
+ restoreTab
3384
+ ] }),
3385
+ document.body
3386
+ );
3387
+ }
3388
+
3389
+ // src/hooks/undoHistory.ts
3390
+ var UNDO_LIMIT = 100;
3391
+ var emptyUndoState = { past: [], future: [] };
3392
+ function undoReducer(state, action) {
3393
+ switch (action.type) {
3394
+ case "record": {
3395
+ const newest = state.past[state.past.length - 1];
3396
+ if (action.step.coalesceKey !== null && newest?.coalesceKey === action.step.coalesceKey) {
3397
+ return state.future.length === 0 ? state : { ...state, future: [] };
3398
+ }
3399
+ const past = [...state.past, action.step];
3400
+ return {
3401
+ past: past.length > UNDO_LIMIT ? past.slice(past.length - UNDO_LIMIT) : past,
3402
+ future: []
3403
+ };
3404
+ }
3405
+ case "undo": {
3406
+ if (state.past.length === 0) return state;
3407
+ return {
3408
+ past: state.past.slice(0, -1),
3409
+ future: [action.redoStep, ...state.future]
3410
+ };
3411
+ }
3412
+ case "redo": {
3413
+ if (state.future.length === 0) return state;
3414
+ return {
3415
+ past: [...state.past, action.undoStep],
3416
+ future: state.future.slice(1)
3417
+ };
3418
+ }
3419
+ case "clear":
3420
+ return state.past.length === 0 && state.future.length === 0 ? state : emptyUndoState;
3421
+ }
3422
+ }
3423
+ var TEXT_ENTRY = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
3424
+ function matchUndoHotkey(e) {
3425
+ if (!(e.metaKey || e.ctrlKey) || e.altKey) return null;
3426
+ const key = e.key.toLowerCase();
3427
+ const action = key === "z" ? e.shiftKey ? "redo" : "undo" : key === "y" && e.ctrlKey && !e.metaKey && !e.shiftKey ? "redo" : null;
3428
+ if (!action) return null;
3429
+ const target = e.target;
3430
+ if (target?.isContentEditable) return null;
3431
+ if (target?.tagName && TEXT_ENTRY.has(target.tagName.toUpperCase())) return null;
3432
+ return action;
3433
+ }
3434
+ function sameValue(a, b) {
3435
+ if (Object.is(a, b)) return true;
3436
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
3437
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
3438
+ try {
3439
+ return JSON.stringify(a) === JSON.stringify(b);
3440
+ } catch {
3441
+ return false;
3442
+ }
3443
+ }
3444
+ var RUNAWAY_LIMIT = 500;
3445
+ var RUNAWAY_QUIET_MS = 250;
3446
+ function UndoProvider({ children, canEdit = true, perms, windowId }) {
3447
+ const [state, dispatch] = useReducer(undoReducer, emptyUndoState);
3448
+ const slices = useRef(/* @__PURE__ */ new Map());
3449
+ const [sliceCount, setSliceCount] = useState(0);
3450
+ const hasState = sliceCount > 0;
3451
+ const [readOnlyClaims, setReadOnlyClaims] = useState(0);
3452
+ const [editClaims, setEditClaims] = useState(0);
3453
+ const declaredEditable = editClaims > 0;
3454
+ const { hasAnyPerm } = useShellAuth();
3455
+ const enabled = canEdit && readOnlyClaims === 0 && (perms && perms.length > 0 ? hasAnyPerm(perms) : true);
3456
+ const pending = useRef(null);
3457
+ const suspended = useRef(false);
3458
+ const snapshot = useCallback(() => {
3459
+ const values = {};
3460
+ for (const [id, slice] of slices.current) values[id] = slice.getLast();
3461
+ return values;
3462
+ }, []);
3463
+ const register = useCallback((id, slice) => {
3464
+ if (!slices.current.has(id)) setSliceCount((c) => c + 1);
3465
+ slices.current.set(id, slice);
3466
+ }, []);
3467
+ const unregister = useCallback((id) => {
3468
+ if (slices.current.delete(id)) setSliceCount((c) => c - 1);
3469
+ }, []);
3470
+ const claimReadOnly = useCallback((on) => {
3471
+ setReadOnlyClaims((c) => c + (on ? 1 : -1));
3472
+ }, []);
3473
+ const claimEditable = useCallback((on) => {
3474
+ setEditClaims((c) => c + (on ? 1 : -1));
3475
+ }, []);
3476
+ const [ownMountClaims, setOwnMountClaims] = useState(0);
3477
+ const handMounted = ownMountClaims > 0;
3478
+ const claimOwnMount = useCallback((on) => {
3479
+ setOwnMountClaims((c) => c + (on ? 1 : -1));
3480
+ }, []);
3481
+ const parent = useContext(UndoContext);
3482
+ const parentClaimReadOnly = parent?.claimReadOnly;
3483
+ useEffect(() => {
3484
+ if (!parentClaimReadOnly || enabled) return;
3485
+ parentClaimReadOnly(true);
3486
+ return () => parentClaimReadOnly(false);
3487
+ }, [parentClaimReadOnly, enabled]);
3488
+ const parentClaimOwnMount = parent?.claimOwnMount;
3489
+ useEffect(() => {
3490
+ if (!parentClaimOwnMount || !handMounted) return;
3491
+ parentClaimOwnMount(true);
3492
+ return () => parentClaimOwnMount(false);
3493
+ }, [parentClaimOwnMount, handMounted]);
3494
+ const runaway = useRef({ last: 0, count: 0, tripped: false });
3495
+ const seeded = useRef(false);
3496
+ const record = useCallback((label, coalesceKey, change) => {
3497
+ if (suspended.current) {
3498
+ if (!change || !sameValue(change.prev, change.next)) seeded.current = true;
3499
+ return;
3500
+ }
3501
+ if (!enabled || pending.current) return;
3502
+ const r = runaway.current;
3503
+ if (r.tripped) return;
3504
+ const now = Date.now();
3505
+ if (now - r.last > RUNAWAY_QUIET_MS) r.count = 0;
3506
+ r.last = now;
3507
+ if (++r.count > RUNAWAY_LIMIT) {
3508
+ r.tripped = true;
3509
+ console.error(
3510
+ `[react-os-shell] UndoProvider: "${label}" recorded ${RUNAWAY_LIMIT} undo steps with no pause between them, which no user action can do. The value passed to useUndoable for this slice is almost certainly rebuilt on every render (a .map/.filter/object literal in the call) rather than held in state, so it reads as changed every time \u2014 and since recording a step re-renders the form, that is a loop. Undo recording is now off for this form to keep the tab responsive.`
3511
+ );
3512
+ return;
3513
+ }
3514
+ pending.current = { values: snapshot(), label, coalesceKey };
3515
+ queueMicrotask(() => {
3516
+ const step = pending.current;
3517
+ pending.current = null;
3518
+ if (step) dispatch({ type: "record", step });
3519
+ });
3520
+ }, [enabled, snapshot]);
3521
+ const applyValues = useCallback((values) => {
3522
+ for (const [id, value2] of Object.entries(values)) {
3523
+ const slice = slices.current.get(id);
3524
+ if (!slice) continue;
3525
+ slice.setLast(value2);
3526
+ slice.apply(value2);
3527
+ }
3528
+ }, []);
3529
+ const undo = useCallback(() => {
3530
+ const step = state.past[state.past.length - 1];
3531
+ if (!step) return;
3532
+ const redoStep = { values: snapshot(), label: step.label, coalesceKey: null };
3533
+ applyValues(step.values);
3534
+ dispatch({ type: "undo", redoStep });
3535
+ }, [state.past, snapshot, applyValues]);
3536
+ const redo = useCallback(() => {
3537
+ const step = state.future[0];
3538
+ if (!step) return;
3539
+ const undoStep = { values: snapshot(), label: step.label, coalesceKey: null };
3540
+ applyValues(step.values);
3541
+ dispatch({ type: "redo", undoStep });
3542
+ }, [state.future, snapshot, applyValues]);
3543
+ const [baselineToken, setBaselineToken] = useState(0);
3544
+ const tokenCounter = useRef(0);
3545
+ const suspendUntil = useRef(0);
3546
+ const forceClear = useRef(false);
3547
+ const suspend = useCallback(() => {
3548
+ suspended.current = true;
3549
+ seeded.current = false;
3550
+ pending.current = null;
3551
+ tokenCounter.current += 1;
3552
+ suspendUntil.current = tokenCounter.current;
3553
+ setBaselineToken(tokenCounter.current);
3554
+ }, []);
3555
+ const baselineKey = useRef(void 0);
3556
+ const baseline = useCallback((key) => {
3557
+ if (key !== void 0) {
3558
+ if (baselineKey.current !== void 0 && key !== baselineKey.current) forceClear.current = true;
3559
+ baselineKey.current = key;
3560
+ }
3561
+ suspend();
3562
+ }, [suspend]);
3563
+ useEffect(() => {
3564
+ if (!suspended.current || baselineToken !== suspendUntil.current) return;
3565
+ suspended.current = false;
3566
+ pending.current = null;
3567
+ if (seeded.current || forceClear.current) dispatch({ type: "clear" });
3568
+ seeded.current = false;
3569
+ forceClear.current = false;
3570
+ }, [baselineToken]);
3571
+ const clear = useCallback(() => {
3572
+ forceClear.current = true;
3573
+ dispatch({ type: "clear" });
3574
+ suspend();
3575
+ }, [suspend]);
3576
+ const canUndo = enabled && state.past.length > 0;
3577
+ const canRedo = enabled && state.future.length > 0;
3578
+ const isActive = useIsActiveWindow(windowId);
3579
+ const keyed = useRef({ undo, redo, canUndo, canRedo });
3580
+ keyed.current = { undo, redo, canUndo, canRedo };
3581
+ useEffect(() => {
3582
+ if (!isActive || !enabled) return;
3583
+ const handler = (e) => {
3584
+ const action = matchUndoHotkey(e);
3585
+ if (!action) return;
3586
+ const k = keyed.current;
3587
+ if (action === "undo" ? !k.canUndo : !k.canRedo) return;
3588
+ e.preventDefault();
3589
+ if (action === "undo") k.undo();
3590
+ else k.redo();
3591
+ };
3592
+ window.addEventListener("keydown", handler);
3593
+ return () => window.removeEventListener("keydown", handler);
3594
+ }, [isActive, enabled]);
3595
+ const value = useMemo(() => ({
3596
+ register,
3597
+ unregister,
3598
+ record,
3599
+ undo,
3600
+ redo,
3601
+ clear,
3602
+ baseline,
3603
+ canUndo,
3604
+ canRedo,
3605
+ enabled,
3606
+ hasState,
3607
+ handMounted,
3608
+ claimOwnMount,
3609
+ claimReadOnly,
3610
+ declaredEditable,
3611
+ claimEditable,
3612
+ undoLabel: canUndo ? state.past[state.past.length - 1].label : null,
3613
+ redoLabel: canRedo ? state.future[0].label : null
3614
+ }), [
3615
+ register,
3616
+ unregister,
3617
+ record,
3618
+ undo,
3619
+ redo,
3620
+ clear,
3621
+ baseline,
3622
+ canUndo,
3623
+ canRedo,
3624
+ enabled,
3625
+ hasState,
3626
+ handMounted,
3627
+ claimOwnMount,
3628
+ claimReadOnly,
3629
+ declaredEditable,
3630
+ claimEditable,
3631
+ state.past,
3632
+ state.future
3633
+ ]);
3634
+ const insideModal = useEnclosingModalId() !== "";
3635
+ const footerOpen = useModalFooterOpen();
3636
+ const showsOwnControls = insideModal && enabled && hasState && !handMounted && footerOpen;
3637
+ return /* @__PURE__ */ jsxs(UndoContext.Provider, { value, children: [
3638
+ showsOwnControls && /* @__PURE__ */ jsx(ModalActions, { position: "left", quiet: true, children: /* @__PURE__ */ jsx(UndoControls, { auto: true }) }),
3639
+ children
3640
+ ] });
3641
+ }
3642
+ function useUndo() {
3643
+ const ctx = useContext(UndoContext);
3644
+ const noop = useCallback(() => {
3645
+ }, []);
3646
+ return {
3647
+ undo: ctx?.undo ?? noop,
3648
+ redo: ctx?.redo ?? noop,
3649
+ clear: ctx?.clear ?? noop,
3650
+ baseline: ctx?.baseline ?? noop,
3651
+ canUndo: ctx?.canUndo ?? false,
3652
+ canRedo: ctx?.canRedo ?? false,
3653
+ undoLabel: ctx?.undoLabel ?? null,
3654
+ redoLabel: ctx?.redoLabel ?? null,
3655
+ enabled: ctx?.enabled ?? false,
3656
+ hasState: ctx?.hasState ?? false
3657
+ };
3658
+ }
3659
+ function useUndoCanEdit(canEdit) {
3660
+ const ctx = useContext(UndoContext);
3661
+ const claimReadOnly = ctx?.claimReadOnly;
3662
+ const claimEditable = ctx?.claimEditable;
3663
+ useEffect(() => {
3664
+ const claim = canEdit ? claimEditable : claimReadOnly;
3665
+ if (!claim) return;
3666
+ claim(true);
3667
+ return () => claim(false);
3668
+ }, [claimReadOnly, claimEditable, canEdit]);
3669
+ }
3670
+ function useUndoable(value, apply, opts) {
3671
+ const ctx = useContext(UndoContext);
3672
+ const id = useId();
3673
+ const last = useRef(value);
3674
+ const applyRef = useRef(apply);
3675
+ applyRef.current = apply;
3676
+ useEffect(() => {
3677
+ if (!ctx) return;
3678
+ ctx.register(id, {
3679
+ getLast: () => last.current,
3680
+ setLast: (v) => {
3681
+ last.current = v;
3682
+ },
3683
+ apply: (v) => applyRef.current(v)
3684
+ });
3685
+ return () => ctx.unregister(id);
3686
+ }, [ctx, id]);
3687
+ const { label, coalesceKey = null } = opts;
3688
+ const ctxRef = useRef(ctx);
3689
+ ctxRef.current = ctx;
3690
+ const optsRef = useRef({ label, coalesceKey });
3691
+ optsRef.current = { label, coalesceKey };
3692
+ useEffect(() => {
3693
+ const c = ctxRef.current;
3694
+ if (!c) return;
3695
+ if (Object.is(value, last.current)) return;
3696
+ c.record(optsRef.current.label, optsRef.current.coalesceKey, { prev: last.current, next: value });
3697
+ last.current = value;
3698
+ }, [value]);
3699
+ }
3700
+ function useUndoableState(initial, opts) {
3701
+ const [value, setValue] = useState(initial);
3702
+ useUndoable(value, (next) => setValue(() => next), opts);
3703
+ return [value, setValue];
3704
+ }
3705
+
3706
+ export { CancelButton, CopyButton, DocFavStar, EntityWindowFallback, EntityWindowLoading, EntityWindowState, LoadingSpinner2 as LoadingSpinner, MODAL_SUBMIT_SELECTOR, Modal, ModalActions, SIDEBAR_STRIP_W, UndoControls, UndoProvider, WINDOW_REGISTRY, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, beginWindowGesture, cancelWindowFront, client_default, commitExposeHighlight, drainPerfEvents, exitExposeMode, forgetMaximizedWindowBoxes, getActiveModalId, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isDraftEntityId, isDraftEntityWindow, isEntityEntry, isPageEntry, isShellApiClientConfigured, markMenuOpen, requestWindowFront, setExposeHighlight, setPerfCollecting, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useEnclosingModalId, useIsActiveWindow, useModalActive, useUndo, useUndoCanEdit, useUndoable, useUndoableState, useWidgetSettings, useWindowDirty, useWindowManager, useWindowMenuItem, useWindowTitle };
3707
+ //# sourceMappingURL=chunk-W6XTG23K.js.map
3708
+ //# sourceMappingURL=chunk-W6XTG23K.js.map