react-os-shell 4.14.0 → 4.20.0

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 (95) hide show
  1. package/README.md +46 -0
  2. package/dist/{Browser-SFNEZ6KW.js → Browser-2TRNQ4MK.js} +9 -6
  3. package/dist/Browser-2TRNQ4MK.js.map +1 -0
  4. package/dist/{Calculator-SJBZNZWR.js → Calculator-QCAJ6ITI.js} +8 -5
  5. package/dist/Calculator-QCAJ6ITI.js.map +1 -0
  6. package/dist/ConfirmDialog-WAM5OKAV.js +3 -0
  7. package/dist/{ConfirmDialog-LZ2JQBHA.js.map → ConfirmDialog-WAM5OKAV.js.map} +1 -1
  8. package/dist/{CurrencyConverter-5OUQSITK.js → CurrencyConverter-PCYC6RL5.js} +8 -5
  9. package/dist/CurrencyConverter-PCYC6RL5.js.map +1 -0
  10. package/dist/{Documents-IUV2A2R6.js → Documents-O5TEGSYH.js} +10 -7
  11. package/dist/Documents-O5TEGSYH.js.map +1 -0
  12. package/dist/{Files-RZSUSCOS.js → Files-CD3FTPLK.js} +16 -12
  13. package/dist/Files-CD3FTPLK.js.map +1 -0
  14. package/dist/{Notepad-DFPJKEBG.js → Notepad-5KZS7PUJ.js} +10 -7
  15. package/dist/Notepad-5KZS7PUJ.js.map +1 -0
  16. package/dist/{PomodoroTimer-5H3Q5SNS.js → PomodoroTimer-YABFEPEV.js} +10 -7
  17. package/dist/PomodoroTimer-YABFEPEV.js.map +1 -0
  18. package/dist/{Preview-JTMOV35N.js → Preview-UUNXWQME.js} +11 -8
  19. package/dist/Preview-UUNXWQME.js.map +1 -0
  20. package/dist/{Sidebar-IMACKSHS.js → Sidebar-B3LY5T2U.js} +5 -4
  21. package/dist/Sidebar-B3LY5T2U.js.map +1 -0
  22. package/dist/{Spreadsheet-2R43CIYR.js → Spreadsheet-DB5GMW2X.js} +10 -7
  23. package/dist/Spreadsheet-DB5GMW2X.js.map +1 -0
  24. package/dist/{Stock-FLWCMQZR.js → Stock-IPLLRUOC.js} +8 -5
  25. package/dist/Stock-IPLLRUOC.js.map +1 -0
  26. package/dist/{Weather-DKWGAHWC.js → Weather-SI5QN7X2.js} +8 -5
  27. package/dist/Weather-SI5QN7X2.js.map +1 -0
  28. package/dist/{WorldClock-5UXXPCBB.js → WorldClock-WYDSMUWX.js} +8 -5
  29. package/dist/WorldClock-WYDSMUWX.js.map +1 -0
  30. package/dist/apps/index.js +22 -19
  31. package/dist/apps/index.js.map +1 -1
  32. package/dist/chunk-22BNPLM4.js +46 -0
  33. package/dist/chunk-22BNPLM4.js.map +1 -0
  34. package/dist/{chunk-IVLDAUYM.js → chunk-3CUSBZWG.js} +3 -56
  35. package/dist/chunk-3CUSBZWG.js.map +1 -0
  36. package/dist/{chunk-VENYVK3L.js → chunk-4AKVHY4B.js} +48 -12
  37. package/dist/chunk-4AKVHY4B.js.map +1 -0
  38. package/dist/chunk-C22EFSND.js +188 -0
  39. package/dist/chunk-C22EFSND.js.map +1 -0
  40. package/dist/{chunk-AFOTTLRS.js → chunk-CZH7CN3S.js} +5 -5
  41. package/dist/{chunk-AFOTTLRS.js.map → chunk-CZH7CN3S.js.map} +1 -1
  42. package/dist/chunk-EGM44WML.js +5008 -0
  43. package/dist/chunk-EGM44WML.js.map +1 -0
  44. package/dist/{chunk-H4IEH6FK.js → chunk-EWDYCO65.js} +3 -3
  45. package/dist/{chunk-H4IEH6FK.js.map → chunk-EWDYCO65.js.map} +1 -1
  46. package/dist/{chunk-DINNWYVB.js → chunk-HYAW6KIL.js} +4 -45
  47. package/dist/chunk-HYAW6KIL.js.map +1 -0
  48. package/dist/{chunk-CVVS2OJO.js → chunk-IGPSRRL7.js} +266 -715
  49. package/dist/chunk-IGPSRRL7.js.map +1 -0
  50. package/dist/chunk-L2O6B5KT.js +394 -0
  51. package/dist/chunk-L2O6B5KT.js.map +1 -0
  52. package/dist/{chunk-UI43AZAK.js → chunk-OI253GOA.js} +4 -4
  53. package/dist/{chunk-UI43AZAK.js.map → chunk-OI253GOA.js.map} +1 -1
  54. package/dist/{chunk-ZPCGIR62.js → chunk-OQ7GRU7G.js} +3 -3
  55. package/dist/{chunk-ZPCGIR62.js.map → chunk-OQ7GRU7G.js.map} +1 -1
  56. package/dist/{chunk-XJCAUC2Y.js → chunk-Q3AOIJ7B.js} +3 -3
  57. package/dist/{chunk-XJCAUC2Y.js.map → chunk-Q3AOIJ7B.js.map} +1 -1
  58. package/dist/{chunk-N2UW3CAS.js → chunk-QJS6RBH4.js} +3 -3
  59. package/dist/{chunk-N2UW3CAS.js.map → chunk-QJS6RBH4.js.map} +1 -1
  60. package/dist/chunk-WNLPTLT6.js +56 -0
  61. package/dist/chunk-WNLPTLT6.js.map +1 -0
  62. package/dist/{chunk-QU5L5ZYM.js → chunk-X5PZYTAK.js} +3 -3
  63. package/dist/{chunk-QU5L5ZYM.js.map → chunk-X5PZYTAK.js.map} +1 -1
  64. package/dist/chunk-ZKQHO2Z7.js +7 -0
  65. package/dist/{chunk-WDXP2Y4T.js.map → chunk-ZKQHO2Z7.js.map} +1 -1
  66. package/dist/index-C5c6FA0c.d.ts +1931 -0
  67. package/dist/index.d.ts +7 -1375
  68. package/dist/index.js +159 -4082
  69. package/dist/index.js.map +1 -1
  70. package/dist/shell.css +105 -0
  71. package/dist/styles.css +12 -501
  72. package/dist/ui/index.d.ts +3 -0
  73. package/dist/ui/index.js +11 -0
  74. package/dist/ui/index.js.map +1 -0
  75. package/dist/ui.css +425 -0
  76. package/package.json +7 -2
  77. package/dist/Browser-SFNEZ6KW.js.map +0 -1
  78. package/dist/Calculator-SJBZNZWR.js.map +0 -1
  79. package/dist/ConfirmDialog-LZ2JQBHA.js +0 -4
  80. package/dist/CurrencyConverter-5OUQSITK.js.map +0 -1
  81. package/dist/Documents-IUV2A2R6.js.map +0 -1
  82. package/dist/Files-RZSUSCOS.js.map +0 -1
  83. package/dist/Notepad-DFPJKEBG.js.map +0 -1
  84. package/dist/PomodoroTimer-5H3Q5SNS.js.map +0 -1
  85. package/dist/Preview-JTMOV35N.js.map +0 -1
  86. package/dist/Sidebar-IMACKSHS.js.map +0 -1
  87. package/dist/Spreadsheet-2R43CIYR.js.map +0 -1
  88. package/dist/Stock-FLWCMQZR.js.map +0 -1
  89. package/dist/Weather-DKWGAHWC.js.map +0 -1
  90. package/dist/WorldClock-5UXXPCBB.js.map +0 -1
  91. package/dist/chunk-CVVS2OJO.js.map +0 -1
  92. package/dist/chunk-DINNWYVB.js.map +0 -1
  93. package/dist/chunk-IVLDAUYM.js.map +0 -1
  94. package/dist/chunk-VENYVK3L.js.map +0 -1
  95. package/dist/chunk-WDXP2Y4T.js +0 -7
@@ -1,64 +1,12 @@
1
- import { useShellAuth, glassStyle, GLASS_DIVIDER, navIcons } from './chunk-IVLDAUYM.js';
2
- import { createContext, Component, useReducer, useRef, useCallback, useState, useEffect, useMemo, useContext, useId, useLayoutEffect, useSyncExternalStore, isValidElement, cloneElement, Suspense } from 'react';
3
- import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
4
- import { XMarkIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
1
+ import { useIsMobile, useShellPrefs, PopupMenu, PopupMenuItem, PopupMenuDivider } from './chunk-C22EFSND.js';
2
+ import { confirm, runEscapeInterceptors } from './chunk-L2O6B5KT.js';
3
+ import { useShellAuth, navIcons } from './chunk-WNLPTLT6.js';
4
+ import { createContext, Component, useContext, useRef, useLayoutEffect, useState, useEffect, useCallback, useSyncExternalStore, useMemo, isValidElement, cloneElement, useReducer, useId, Suspense } from 'react';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
6
  import { createPortal } from 'react-dom';
7
+ import { XMarkIcon } from '@heroicons/react/24/outline';
6
8
  import { useLocation } from 'react-router-dom';
7
9
  import { useQueryClient, useQuery } from '@tanstack/react-query';
8
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
-
10
- // src/api/client.ts
11
- var _apiClient = null;
12
- function setShellApiClient(instance) {
13
- _apiClient = instance;
14
- }
15
- function isShellApiClientConfigured() {
16
- return _apiClient !== null;
17
- }
18
- var NOOP_METHODS = /* @__PURE__ */ new Set([
19
- "get",
20
- "post",
21
- "put",
22
- "patch",
23
- "delete",
24
- "head",
25
- "options",
26
- "request"
27
- ]);
28
- var apiClient = new Proxy({}, {
29
- get(_t, prop) {
30
- if (_apiClient) return _apiClient[prop];
31
- if (typeof prop === "string" && NOOP_METHODS.has(prop)) {
32
- return () => Promise.resolve({ data: null, status: 0, statusText: "", headers: {}, config: {} });
33
- }
34
- return void 0;
35
- }
36
- });
37
- var client_default = apiClient;
38
-
39
- // src/shell/entityFetchPolicy.ts
40
- var RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([
41
- 408,
42
- // Request Timeout
43
- 429
44
- // Too Many Requests
45
- ]);
46
- var ENTITY_FETCH_MAX_RETRIES = 3;
47
- var ENTITY_REFETCH_INTERVAL_MS = 6e4;
48
- function isPermanentClientError(error) {
49
- const status = error?.response?.status;
50
- if (typeof status !== "number") return false;
51
- return status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
52
- }
53
- function shouldRetryEntityFetch(failureCount, error) {
54
- return !isPermanentClientError(error) && failureCount < ENTITY_FETCH_MAX_RETRIES;
55
- }
56
- function entityRefetchInterval(error) {
57
- return isPermanentClientError(error) ? false : ENTITY_REFETCH_INTERVAL_MS;
58
- }
59
- function entityDetailUrl(endpoint, entityId) {
60
- return `${endpoint}${encodeURIComponent(entityId)}/`;
61
- }
62
10
 
63
11
  // src/windowRegistry/types.ts
64
12
  function isPageEntry(entry) {
@@ -88,6 +36,35 @@ var WINDOW_REGISTRY = new Proxy({}, {
88
36
  return void 0;
89
37
  }
90
38
  });
39
+
40
+ // src/api/client.ts
41
+ var _apiClient = null;
42
+ function setShellApiClient(instance) {
43
+ _apiClient = instance;
44
+ }
45
+ function isShellApiClientConfigured() {
46
+ return _apiClient !== null;
47
+ }
48
+ var NOOP_METHODS = /* @__PURE__ */ new Set([
49
+ "get",
50
+ "post",
51
+ "put",
52
+ "patch",
53
+ "delete",
54
+ "head",
55
+ "options",
56
+ "request"
57
+ ]);
58
+ var apiClient = new Proxy({}, {
59
+ get(_t, prop) {
60
+ if (_apiClient) return _apiClient[prop];
61
+ if (typeof prop === "string" && NOOP_METHODS.has(prop)) {
62
+ return () => Promise.resolve({ data: null, status: 0, statusText: "", headers: {}, config: {} });
63
+ }
64
+ return void 0;
65
+ }
66
+ });
67
+ var client_default = apiClient;
91
68
  function WindowCrashedFallback({ error, onReload }) {
92
69
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-3 px-6 py-12 text-center", children: [
93
70
  /* @__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" }) }),
@@ -124,215 +101,28 @@ var WindowErrorBoundary = class extends Component {
124
101
  }
125
102
  };
126
103
 
127
- // src/hooks/undoHistory.ts
128
- var UNDO_LIMIT = 100;
129
- var emptyUndoState = { past: [], future: [] };
130
- function undoReducer(state, action) {
131
- switch (action.type) {
132
- case "record": {
133
- const newest = state.past[state.past.length - 1];
134
- if (action.step.coalesceKey !== null && newest?.coalesceKey === action.step.coalesceKey) {
135
- return state.future.length === 0 ? state : { ...state, future: [] };
136
- }
137
- const past = [...state.past, action.step];
138
- return {
139
- past: past.length > UNDO_LIMIT ? past.slice(past.length - UNDO_LIMIT) : past,
140
- future: []
141
- };
142
- }
143
- case "undo": {
144
- if (state.past.length === 0) return state;
145
- return {
146
- past: state.past.slice(0, -1),
147
- future: [action.redoStep, ...state.future]
148
- };
149
- }
150
- case "redo": {
151
- if (state.future.length === 0) return state;
152
- return {
153
- past: [...state.past, action.undoStep],
154
- future: state.future.slice(1)
155
- };
156
- }
157
- case "clear":
158
- return state.past.length === 0 && state.future.length === 0 ? state : emptyUndoState;
159
- }
160
- }
161
- var TEXT_ENTRY = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
162
- function matchUndoHotkey(e) {
163
- if (!(e.metaKey || e.ctrlKey) || e.altKey) return null;
164
- const key = e.key.toLowerCase();
165
- const action = key === "z" ? e.shiftKey ? "redo" : "undo" : key === "y" && e.ctrlKey && !e.metaKey && !e.shiftKey ? "redo" : null;
166
- if (!action) return null;
167
- const target = e.target;
168
- if (target?.isContentEditable) return null;
169
- if (target?.tagName && TEXT_ENTRY.has(target.tagName.toUpperCase())) return null;
170
- return action;
171
- }
172
- var UndoContext = createContext(null);
173
- var RUNAWAY_LIMIT = 500;
174
- var RUNAWAY_QUIET_MS = 250;
175
- function UndoProvider({ children, canEdit = true, perms, windowId }) {
176
- const [state, dispatch] = useReducer(undoReducer, emptyUndoState);
177
- const slices = useRef(/* @__PURE__ */ new Map());
178
- const { hasAnyPerm } = useShellAuth();
179
- const enabled = canEdit && (perms && perms.length > 0 ? hasAnyPerm(perms) : true);
180
- const pending = useRef(null);
181
- const suspended = useRef(false);
182
- const snapshot = useCallback(() => {
183
- const values = {};
184
- for (const [id, slice] of slices.current) values[id] = slice.getLast();
185
- return values;
186
- }, []);
187
- const register = useCallback((id, slice) => {
188
- slices.current.set(id, slice);
189
- }, []);
190
- const unregister = useCallback((id) => {
191
- slices.current.delete(id);
192
- }, []);
193
- const runaway = useRef({ last: 0, count: 0, tripped: false });
194
- const record = useCallback((label, coalesceKey) => {
195
- if (!enabled || suspended.current || pending.current) return;
196
- const r = runaway.current;
197
- if (r.tripped) return;
198
- const now = Date.now();
199
- if (now - r.last > RUNAWAY_QUIET_MS) r.count = 0;
200
- r.last = now;
201
- if (++r.count > RUNAWAY_LIMIT) {
202
- r.tripped = true;
203
- console.error(
204
- `[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.`
205
- );
206
- return;
207
- }
208
- pending.current = { values: snapshot(), label, coalesceKey };
209
- queueMicrotask(() => {
210
- const step = pending.current;
211
- pending.current = null;
212
- if (step) dispatch({ type: "record", step });
213
- });
214
- }, [enabled, snapshot]);
215
- const applyValues = useCallback((values) => {
216
- for (const [id, value2] of Object.entries(values)) {
217
- const slice = slices.current.get(id);
218
- if (!slice) continue;
219
- slice.setLast(value2);
220
- slice.apply(value2);
221
- }
222
- }, []);
223
- const undo = useCallback(() => {
224
- const step = state.past[state.past.length - 1];
225
- if (!step) return;
226
- const redoStep = { values: snapshot(), label: step.label, coalesceKey: null };
227
- applyValues(step.values);
228
- dispatch({ type: "undo", redoStep });
229
- }, [state.past, snapshot, applyValues]);
230
- const redo = useCallback(() => {
231
- const step = state.future[0];
232
- if (!step) return;
233
- const undoStep = { values: snapshot(), label: step.label, coalesceKey: null };
234
- applyValues(step.values);
235
- dispatch({ type: "redo", undoStep });
236
- }, [state.future, snapshot, applyValues]);
237
- const [baselineToken, setBaselineToken] = useState(0);
238
- const baseline = useCallback(() => {
239
- suspended.current = true;
240
- pending.current = null;
241
- setBaselineToken((t) => t + 1);
242
- dispatch({ type: "clear" });
243
- }, []);
244
- useEffect(() => {
245
- if (!suspended.current) return;
246
- suspended.current = false;
247
- pending.current = null;
248
- dispatch({ type: "clear" });
249
- }, [baselineToken]);
250
- const clear = baseline;
251
- const canUndo = enabled && state.past.length > 0;
252
- const canRedo = enabled && state.future.length > 0;
253
- const isActive = useIsActiveWindow(windowId);
254
- const keyed = useRef({ undo, redo, canUndo, canRedo });
255
- keyed.current = { undo, redo, canUndo, canRedo };
256
- useEffect(() => {
257
- if (!isActive || !enabled) return;
258
- const handler = (e) => {
259
- const action = matchUndoHotkey(e);
260
- if (!action) return;
261
- const k = keyed.current;
262
- if (action === "undo" ? !k.canUndo : !k.canRedo) return;
263
- e.preventDefault();
264
- if (action === "undo") k.undo();
265
- else k.redo();
266
- };
267
- window.addEventListener("keydown", handler);
268
- return () => window.removeEventListener("keydown", handler);
269
- }, [isActive, enabled]);
270
- const value = useMemo(() => ({
271
- register,
272
- unregister,
273
- record,
274
- undo,
275
- redo,
276
- clear,
277
- baseline,
278
- canUndo,
279
- canRedo,
280
- enabled,
281
- undoLabel: canUndo ? state.past[state.past.length - 1].label : null,
282
- redoLabel: canRedo ? state.future[0].label : null
283
- }), [register, unregister, record, undo, redo, clear, baseline, canUndo, canRedo, enabled, state.past, state.future]);
284
- return /* @__PURE__ */ jsx(UndoContext.Provider, { value, children });
104
+ // src/shell/entityFetchPolicy.ts
105
+ var RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([
106
+ 408,
107
+ // Request Timeout
108
+ 429
109
+ // Too Many Requests
110
+ ]);
111
+ var ENTITY_FETCH_MAX_RETRIES = 3;
112
+ var ENTITY_REFETCH_INTERVAL_MS = 6e4;
113
+ function isPermanentClientError(error) {
114
+ const status = error?.response?.status;
115
+ if (typeof status !== "number") return false;
116
+ return status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
285
117
  }
286
- function useUndo() {
287
- const ctx = useContext(UndoContext);
288
- const noop = useCallback(() => {
289
- }, []);
290
- return {
291
- undo: ctx?.undo ?? noop,
292
- redo: ctx?.redo ?? noop,
293
- clear: ctx?.clear ?? noop,
294
- baseline: ctx?.baseline ?? noop,
295
- canUndo: ctx?.canUndo ?? false,
296
- canRedo: ctx?.canRedo ?? false,
297
- undoLabel: ctx?.undoLabel ?? null,
298
- redoLabel: ctx?.redoLabel ?? null,
299
- enabled: ctx?.enabled ?? false
300
- };
118
+ function shouldRetryEntityFetch(failureCount, error) {
119
+ return !isPermanentClientError(error) && failureCount < ENTITY_FETCH_MAX_RETRIES;
301
120
  }
302
- function useUndoable(value, apply, opts) {
303
- const ctx = useContext(UndoContext);
304
- const id = useId();
305
- const last = useRef(value);
306
- const applyRef = useRef(apply);
307
- applyRef.current = apply;
308
- useEffect(() => {
309
- if (!ctx) return;
310
- ctx.register(id, {
311
- getLast: () => last.current,
312
- setLast: (v) => {
313
- last.current = v;
314
- },
315
- apply: (v) => applyRef.current(v)
316
- });
317
- return () => ctx.unregister(id);
318
- }, [ctx, id]);
319
- const { label, coalesceKey = null } = opts;
320
- const ctxRef = useRef(ctx);
321
- ctxRef.current = ctx;
322
- const optsRef = useRef({ label, coalesceKey });
323
- optsRef.current = { label, coalesceKey };
324
- useEffect(() => {
325
- const c = ctxRef.current;
326
- if (!c) return;
327
- if (Object.is(value, last.current)) return;
328
- c.record(optsRef.current.label, optsRef.current.coalesceKey);
329
- last.current = value;
330
- }, [value]);
121
+ function entityRefetchInterval(error) {
122
+ return isPermanentClientError(error) ? false : ENTITY_REFETCH_INTERVAL_MS;
331
123
  }
332
- function useUndoableState(initial, opts) {
333
- const [value, setValue] = useState(initial);
334
- useUndoable(value, (next) => setValue(() => next), opts);
335
- return [value, setValue];
124
+ function entityDetailUrl(endpoint, entityId) {
125
+ return `${endpoint}${encodeURIComponent(entityId)}/`;
336
126
  }
337
127
  function LoadingSpinner() {
338
128
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-8 text-sm text-gray-400", children: "Loading..." });
@@ -1123,151 +913,6 @@ function WindowManagerProvider({ children, windowAccentForRoute }) {
1123
913
  })
1124
914
  ] });
1125
915
  }
1126
- function getDensity() {
1127
- return getComputedStyle(document.documentElement).getPropertyValue("--menu-density")?.trim() || "normal";
1128
- }
1129
- function PopupMenu({ children, style, className = "", onClose, minWidth = 180, portal = false }) {
1130
- const ref = useRef(null);
1131
- useEffect(() => {
1132
- if (!onClose) return;
1133
- const handleClick = (e) => {
1134
- if (e.target.closest("[data-menu-toggle]")) return;
1135
- if (ref.current && !ref.current.contains(e.target)) onClose();
1136
- };
1137
- const handleKey = (e) => {
1138
- if (e.key === "Escape") onClose();
1139
- };
1140
- window.addEventListener("pointerdown", handleClick);
1141
- window.addEventListener("keydown", handleKey);
1142
- return () => {
1143
- window.removeEventListener("pointerdown", handleClick);
1144
- window.removeEventListener("keydown", handleKey);
1145
- };
1146
- }, [onClose]);
1147
- useEffect(() => {
1148
- const el = ref.current;
1149
- if (!el) return;
1150
- requestAnimationFrame(() => {
1151
- const rect = el.getBoundingClientRect();
1152
- const margin = 8;
1153
- if (rect.right > window.innerWidth - margin) {
1154
- const overflow = rect.right - window.innerWidth + margin;
1155
- if (el.style.left) {
1156
- el.style.left = `${parseFloat(el.style.left) - overflow}px`;
1157
- } else if (!el.style.right) {
1158
- el.style.left = `${rect.left - overflow}px`;
1159
- }
1160
- }
1161
- if (rect.left < margin) {
1162
- if (el.style.left) {
1163
- el.style.left = `${margin}px`;
1164
- } else if (el.style.right) {
1165
- el.style.right = `${window.innerWidth - rect.width - margin}px`;
1166
- }
1167
- }
1168
- if (rect.bottom > window.innerHeight - margin) {
1169
- const overflow = rect.bottom - window.innerHeight + margin;
1170
- if (el.style.top) {
1171
- el.style.top = `${parseFloat(el.style.top) - overflow}px`;
1172
- } else if (el.style.bottom) {
1173
- el.style.bottom = `${margin}px`;
1174
- } else {
1175
- el.style.top = `${rect.top - overflow}px`;
1176
- }
1177
- }
1178
- if (rect.top < margin) {
1179
- if (el.style.top) {
1180
- el.style.top = `${margin}px`;
1181
- } else if (el.style.bottom) {
1182
- el.style.bottom = `${window.innerHeight - rect.height - margin}px`;
1183
- }
1184
- }
1185
- });
1186
- });
1187
- const density = getDensity();
1188
- const menu = /* @__PURE__ */ jsxs(
1189
- "div",
1190
- {
1191
- ref,
1192
- className: `fixed z-[400] rounded-2xl ${density === "tight" ? "py-1" : density === "large" ? "py-2" : "py-1.5"} ${className}`,
1193
- style: { minWidth, animation: "popup-in 0.12s ease-out", ...glassStyle(), ...style },
1194
- children: [
1195
- children,
1196
- /* @__PURE__ */ jsx("style", { children: `@keyframes popup-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }` })
1197
- ]
1198
- }
1199
- );
1200
- return portal ? createPortal(menu, document.body) : menu;
1201
- }
1202
- function PopupMenuItem({ onClick, children, className = "", danger, disabled }) {
1203
- const density = getDensity();
1204
- const itemPadY = density === "tight" ? "0.25rem" : density === "large" ? "0.6rem" : "max(0.25rem, calc(var(--menu-padding-y, 0.5rem) - 0.1rem))";
1205
- return /* @__PURE__ */ jsx(
1206
- "button",
1207
- {
1208
- onClick,
1209
- disabled,
1210
- className: `w-full flex items-center gap-2 text-left transition-colors rounded-lg mx-auto
1211
- ${danger ? "text-red-600 hover:bg-red-50" : "text-gray-700 hover:bg-blue-50 hover:text-blue-700"}
1212
- ${disabled ? "opacity-40 cursor-not-allowed" : ""}
1213
- ${className}`,
1214
- style: {
1215
- width: "calc(100% - 8px)",
1216
- marginLeft: 4,
1217
- marginRight: 4,
1218
- fontSize: "var(--menu-font-size, 14px)",
1219
- paddingLeft: "var(--menu-padding-x, 1rem)",
1220
- paddingRight: "var(--menu-padding-x, 1rem)",
1221
- paddingTop: itemPadY,
1222
- paddingBottom: itemPadY
1223
- },
1224
- children
1225
- }
1226
- );
1227
- }
1228
- function PopupMenuDivider() {
1229
- const density = getDensity();
1230
- return /* @__PURE__ */ jsx("div", { className: `border-t ${GLASS_DIVIDER} ${density === "tight" ? "my-0.5" : density === "large" ? "my-1.5" : "my-1"} mx-3` });
1231
- }
1232
- function PopupMenuLabel({ children }) {
1233
- const density = getDensity();
1234
- const labelPadY = density === "tight" ? "0.125rem" : density === "large" ? "0.375rem" : "0.25rem";
1235
- return /* @__PURE__ */ jsx(
1236
- "div",
1237
- {
1238
- className: "text-[10px] font-medium text-gray-400 uppercase tracking-wider",
1239
- style: {
1240
- paddingLeft: "var(--menu-padding-x, 1rem)",
1241
- paddingRight: "var(--menu-padding-x, 1rem)",
1242
- paddingTop: labelPadY,
1243
- paddingBottom: labelPadY
1244
- },
1245
- children
1246
- }
1247
- );
1248
- }
1249
- var QUERY = "(max-width: 767px), (pointer: coarse)";
1250
- function getSnapshot() {
1251
- if (typeof window === "undefined" || !window.matchMedia) return false;
1252
- return window.matchMedia(QUERY).matches;
1253
- }
1254
- function getServerSnapshot() {
1255
- return false;
1256
- }
1257
- function subscribe(cb) {
1258
- if (typeof window === "undefined" || !window.matchMedia) return () => {
1259
- };
1260
- const mql = window.matchMedia(QUERY);
1261
- if (mql.addEventListener) {
1262
- mql.addEventListener("change", cb);
1263
- return () => mql.removeEventListener("change", cb);
1264
- }
1265
- mql.addListener(cb);
1266
- return () => mql.removeListener(cb);
1267
- }
1268
- function useIsMobile() {
1269
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1270
- }
1271
916
 
1272
917
  // src/shell/mobileSwipeStore.ts
1273
918
  var _parentKey = null;
@@ -1321,40 +966,6 @@ function drainPerfEvents() {
1321
966
  counts = empty();
1322
967
  return drained;
1323
968
  }
1324
- var ShellPrefsContext = createContext(null);
1325
- function ShellPrefsProvider({
1326
- value,
1327
- children
1328
- }) {
1329
- return /* @__PURE__ */ jsx(ShellPrefsContext.Provider, { value, children });
1330
- }
1331
- function useLocalStoragePrefs(storageKey = "react-os-shell:prefs", defaults) {
1332
- const [prefs, setPrefs] = useState(() => {
1333
- try {
1334
- const stored = JSON.parse(localStorage.getItem(storageKey) || "{}");
1335
- return { ...defaults ?? {}, ...stored };
1336
- } catch {
1337
- return { ...defaults ?? {} };
1338
- }
1339
- });
1340
- const save = useCallback((patch) => {
1341
- setPrefs((prev) => {
1342
- const next = { ...prev, ...patch };
1343
- try {
1344
- localStorage.setItem(storageKey, JSON.stringify(next));
1345
- } catch {
1346
- }
1347
- return next;
1348
- });
1349
- }, [storageKey]);
1350
- return { prefs, save };
1351
- }
1352
- function useShellPrefs() {
1353
- const ctx = useContext(ShellPrefsContext);
1354
- if (ctx) return ctx;
1355
- return { prefs: {}, save: () => {
1356
- } };
1357
- }
1358
969
 
1359
970
  // src/shell/workArea.ts
1360
971
  var SIDEBAR_STRIP_W = 280;
@@ -1497,7 +1108,7 @@ function CancelButton({ onClick, children, className }) {
1497
1108
  const ctx = useContext(ModalActionsContext);
1498
1109
  const handleClick = async () => {
1499
1110
  if (ctx?.isDirty) {
1500
- const { confirm: confirm2 } = await import('./ConfirmDialog-LZ2JQBHA.js');
1111
+ const { confirm: confirm2 } = await import('./ConfirmDialog-WAM5OKAV.js');
1501
1112
  const ok = await confirm2({
1502
1113
  title: "Discard changes?",
1503
1114
  message: "You have unsaved changes. Are you sure you want to cancel? All changes will be lost.",
@@ -1897,27 +1508,11 @@ function subscribeActive(cb) {
1897
1508
  activeListeners.delete(cb);
1898
1509
  };
1899
1510
  }
1900
- var escapeInterceptors = /* @__PURE__ */ new Set();
1901
- function registerModalEscapeInterceptor(fn) {
1902
- escapeInterceptors.add(fn);
1903
- return () => {
1904
- escapeInterceptors.delete(fn);
1905
- };
1906
- }
1907
1511
  var MODAL_CLOSE_REQUEST_EVENT = "modal-close-request";
1908
1512
  function requestModalClose(windowKey) {
1909
1513
  window.dispatchEvent(new CustomEvent(MODAL_CLOSE_REQUEST_EVENT, {
1910
- detail: { windowKey }
1911
- }));
1912
- }
1913
- function runEscapeInterceptors(e) {
1914
- for (const fn of escapeInterceptors) {
1915
- try {
1916
- if (fn(e)) return true;
1917
- } catch {
1918
- }
1919
- }
1920
- return false;
1514
+ detail: { windowKey }
1515
+ }));
1921
1516
  }
1922
1517
  function useIsActiveModal(modalId) {
1923
1518
  const activeId = useSyncExternalStore(subscribeActive, getActiveModalId);
@@ -2150,6 +1745,7 @@ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = fals
2150
1745
  useEffect(() => {
2151
1746
  setDisplayTitle(title);
2152
1747
  }, [title]);
1748
+ const titleTooltip = useMemo(() => extractTitleText(displayTitle).trim() || void 0, [displayTitle]);
2153
1749
  const [touched, setTouched] = useState(false);
2154
1750
  const [pinnedOnTop, setPinnedOnTop] = useState(false);
2155
1751
  const [windowMenu, setWindowMenu] = useState(null);
@@ -3009,7 +2605,7 @@ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = fals
3009
2605
  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 ${isActive ? "backdrop-blur-sm" : ""}`,
3010
2606
  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))` },
3011
2607
  children: [
3012
- /* @__PURE__ */ jsxs("div", { "data-window-title": true, 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: [
2608
+ /* @__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: [
3013
2609
  !exposeActive && renderIconButton(),
3014
2610
  /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3015
2611
  ] }),
@@ -3038,7 +2634,7 @@ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = fals
3038
2634
  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 ${isActive ? "backdrop-blur-sm" : ""}`,
3039
2635
  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))` },
3040
2636
  children: [
3041
- /* @__PURE__ */ jsxs("div", { "data-window-title": true, 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: [
2637
+ /* @__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: [
3042
2638
  !exposeActive && renderIconButton(),
3043
2639
  /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3044
2640
  ] }),
@@ -3074,7 +2670,7 @@ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = fals
3074
2670
  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 ${isActive ? "backdrop-blur-sm" : ""}`,
3075
2671
  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))` },
3076
2672
  children: [
3077
- /* @__PURE__ */ jsxs("div", { "data-window-title": true, 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: [
2673
+ /* @__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: [
3078
2674
  !exposeActive && renderIconButton(),
3079
2675
  /* @__PURE__ */ jsx("span", { className: "truncate", children: exposeActive ? extractTitleText(displayTitle) : displayTitle })
3080
2676
  ] }),
@@ -3314,263 +2910,218 @@ function Modal({ open, onClose, title, icon, copyText, size = "lg", dirty = fals
3314
2910
  document.body
3315
2911
  );
3316
2912
  }
3317
- var ConfirmContext = createContext(() => Promise.resolve(false));
3318
- var useConfirm = () => useContext(ConfirmContext);
3319
- var globalConfirmFn = () => Promise.resolve(false);
3320
- var confirm = (opts) => globalConfirmFn(opts);
3321
- var globalDestructiveConfirmFn = () => Promise.resolve(false);
3322
- var confirmDestructive = (opts) => globalDestructiveConfirmFn(opts);
3323
- var globalPromptFn = () => Promise.resolve(null);
3324
- var prompt = (opts) => globalPromptFn(opts);
3325
- function ConfirmProvider({ children }) {
3326
- const [open, setOpen] = useState(false);
3327
- const [options, setOptions] = useState({ message: "" });
3328
- const [confirmId, setConfirmId] = useState(0);
3329
- const resolveRef = useRef();
3330
- const activeConfirmIdRef = useRef(null);
3331
- const nextConfirmIdRef = useRef(0);
3332
- const confirmQueueRef = useRef([]);
3333
- const showConfirm = useCallback((request) => {
3334
- activeConfirmIdRef.current = request.id;
3335
- resolveRef.current = request.resolve;
3336
- setConfirmId(request.id);
3337
- setOptions(request.options);
3338
- setOpen(true);
2913
+
2914
+ // src/hooks/undoHistory.ts
2915
+ var UNDO_LIMIT = 100;
2916
+ var emptyUndoState = { past: [], future: [] };
2917
+ function undoReducer(state, action) {
2918
+ switch (action.type) {
2919
+ case "record": {
2920
+ const newest = state.past[state.past.length - 1];
2921
+ if (action.step.coalesceKey !== null && newest?.coalesceKey === action.step.coalesceKey) {
2922
+ return state.future.length === 0 ? state : { ...state, future: [] };
2923
+ }
2924
+ const past = [...state.past, action.step];
2925
+ return {
2926
+ past: past.length > UNDO_LIMIT ? past.slice(past.length - UNDO_LIMIT) : past,
2927
+ future: []
2928
+ };
2929
+ }
2930
+ case "undo": {
2931
+ if (state.past.length === 0) return state;
2932
+ return {
2933
+ past: state.past.slice(0, -1),
2934
+ future: [action.redoStep, ...state.future]
2935
+ };
2936
+ }
2937
+ case "redo": {
2938
+ if (state.future.length === 0) return state;
2939
+ return {
2940
+ past: [...state.past, action.undoStep],
2941
+ future: state.future.slice(1)
2942
+ };
2943
+ }
2944
+ case "clear":
2945
+ return state.past.length === 0 && state.future.length === 0 ? state : emptyUndoState;
2946
+ }
2947
+ }
2948
+ var TEXT_ENTRY = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
2949
+ function matchUndoHotkey(e) {
2950
+ if (!(e.metaKey || e.ctrlKey) || e.altKey) return null;
2951
+ const key = e.key.toLowerCase();
2952
+ const action = key === "z" ? e.shiftKey ? "redo" : "undo" : key === "y" && e.ctrlKey && !e.metaKey && !e.shiftKey ? "redo" : null;
2953
+ if (!action) return null;
2954
+ const target = e.target;
2955
+ if (target?.isContentEditable) return null;
2956
+ if (target?.tagName && TEXT_ENTRY.has(target.tagName.toUpperCase())) return null;
2957
+ return action;
2958
+ }
2959
+ var UndoContext = createContext(null);
2960
+ var RUNAWAY_LIMIT = 500;
2961
+ var RUNAWAY_QUIET_MS = 250;
2962
+ function UndoProvider({ children, canEdit = true, perms, windowId }) {
2963
+ const [state, dispatch] = useReducer(undoReducer, emptyUndoState);
2964
+ const slices = useRef(/* @__PURE__ */ new Map());
2965
+ const { hasAnyPerm } = useShellAuth();
2966
+ const enabled = canEdit && (perms && perms.length > 0 ? hasAnyPerm(perms) : true);
2967
+ const pending = useRef(null);
2968
+ const suspended = useRef(false);
2969
+ const snapshot = useCallback(() => {
2970
+ const values = {};
2971
+ for (const [id, slice] of slices.current) values[id] = slice.getLast();
2972
+ return values;
3339
2973
  }, []);
3340
- const confirmFn = useCallback((opts) => {
3341
- const normalized = typeof opts === "string" ? { message: opts } : opts;
3342
- return new Promise((resolve) => {
3343
- const request = { id: ++nextConfirmIdRef.current, options: normalized, resolve };
3344
- if (activeConfirmIdRef.current === null) showConfirm(request);
3345
- else confirmQueueRef.current.push(request);
3346
- });
3347
- }, [showConfirm]);
3348
- const [dOpen, setDOpen] = useState(false);
3349
- const [dOptions, setDOptions] = useState({ message: "", confirmWord: "Delete" });
3350
- const [dInput, setDInput] = useState("");
3351
- const dResolveRef = useRef();
3352
- const destructiveConfirmFn = useCallback((opts) => {
3353
- setDOptions(opts);
3354
- setDInput("");
3355
- setDOpen(true);
3356
- return new Promise((resolve) => {
3357
- dResolveRef.current = resolve;
3358
- });
2974
+ const register = useCallback((id, slice) => {
2975
+ slices.current.set(id, slice);
3359
2976
  }, []);
3360
- const [pOpen, setPOpen] = useState(false);
3361
- const [pOptions, setPOptions] = useState({});
3362
- const [pInput, setPInput] = useState("");
3363
- const pResolveRef = useRef();
3364
- const promptFn = useCallback((opts) => {
3365
- const normalized = typeof opts === "string" ? { message: opts } : opts;
3366
- setPOptions(normalized);
3367
- setPInput(normalized.defaultValue ?? "");
3368
- setPOpen(true);
3369
- return new Promise((resolve) => {
3370
- pResolveRef.current = resolve;
3371
- });
2977
+ const unregister = useCallback((id) => {
2978
+ slices.current.delete(id);
3372
2979
  }, []);
3373
- useEffect(() => {
3374
- globalConfirmFn = confirmFn;
3375
- globalDestructiveConfirmFn = destructiveConfirmFn;
3376
- globalPromptFn = promptFn;
3377
- }, [confirmFn, destructiveConfirmFn, promptFn]);
3378
- const handleClose = (id, result) => {
3379
- if (activeConfirmIdRef.current !== id) return;
3380
- const resolve = resolveRef.current;
3381
- const next = confirmQueueRef.current.shift();
3382
- if (next) showConfirm(next);
3383
- else {
3384
- activeConfirmIdRef.current = null;
3385
- resolveRef.current = void 0;
3386
- setOpen(false);
2980
+ const runaway = useRef({ last: 0, count: 0, tripped: false });
2981
+ const record = useCallback((label, coalesceKey) => {
2982
+ if (!enabled || suspended.current || pending.current) return;
2983
+ const r = runaway.current;
2984
+ if (r.tripped) return;
2985
+ const now = Date.now();
2986
+ if (now - r.last > RUNAWAY_QUIET_MS) r.count = 0;
2987
+ r.last = now;
2988
+ if (++r.count > RUNAWAY_LIMIT) {
2989
+ r.tripped = true;
2990
+ console.error(
2991
+ `[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.`
2992
+ );
2993
+ return;
3387
2994
  }
3388
- resolve?.(result);
3389
- };
3390
- const handleDClose = (result) => {
3391
- setDOpen(false);
3392
- setDInput("");
3393
- dResolveRef.current?.(result);
3394
- };
3395
- const handlePClose = (commit) => {
3396
- if (commit) {
3397
- const trimmed = pInput.trim();
3398
- if (!trimmed && !pOptions.allowEmpty) {
3399
- setPOpen(false);
3400
- pResolveRef.current?.(null);
3401
- return;
3402
- }
3403
- setPOpen(false);
3404
- pResolveRef.current?.(trimmed);
3405
- } else {
3406
- setPOpen(false);
3407
- pResolveRef.current?.(null);
2995
+ pending.current = { values: snapshot(), label, coalesceKey };
2996
+ queueMicrotask(() => {
2997
+ const step = pending.current;
2998
+ pending.current = null;
2999
+ if (step) dispatch({ type: "record", step });
3000
+ });
3001
+ }, [enabled, snapshot]);
3002
+ const applyValues = useCallback((values) => {
3003
+ for (const [id, value2] of Object.entries(values)) {
3004
+ const slice = slices.current.get(id);
3005
+ if (!slice) continue;
3006
+ slice.setLast(value2);
3007
+ slice.apply(value2);
3408
3008
  }
3009
+ }, []);
3010
+ const undo = useCallback(() => {
3011
+ const step = state.past[state.past.length - 1];
3012
+ if (!step) return;
3013
+ const redoStep = { values: snapshot(), label: step.label, coalesceKey: null };
3014
+ applyValues(step.values);
3015
+ dispatch({ type: "undo", redoStep });
3016
+ }, [state.past, snapshot, applyValues]);
3017
+ const redo = useCallback(() => {
3018
+ const step = state.future[0];
3019
+ if (!step) return;
3020
+ const undoStep = { values: snapshot(), label: step.label, coalesceKey: null };
3021
+ applyValues(step.values);
3022
+ dispatch({ type: "redo", undoStep });
3023
+ }, [state.future, snapshot, applyValues]);
3024
+ const [baselineToken, setBaselineToken] = useState(0);
3025
+ const baseline = useCallback(() => {
3026
+ suspended.current = true;
3027
+ pending.current = null;
3028
+ setBaselineToken((t) => t + 1);
3029
+ dispatch({ type: "clear" });
3030
+ }, []);
3031
+ useEffect(() => {
3032
+ if (!suspended.current) return;
3033
+ suspended.current = false;
3034
+ pending.current = null;
3035
+ dispatch({ type: "clear" });
3036
+ }, [baselineToken]);
3037
+ const clear = baseline;
3038
+ const canUndo = enabled && state.past.length > 0;
3039
+ const canRedo = enabled && state.future.length > 0;
3040
+ const isActive = useIsActiveWindow(windowId);
3041
+ const keyed = useRef({ undo, redo, canUndo, canRedo });
3042
+ keyed.current = { undo, redo, canUndo, canRedo };
3043
+ useEffect(() => {
3044
+ if (!isActive || !enabled) return;
3045
+ const handler = (e) => {
3046
+ const action = matchUndoHotkey(e);
3047
+ if (!action) return;
3048
+ const k = keyed.current;
3049
+ if (action === "undo" ? !k.canUndo : !k.canRedo) return;
3050
+ e.preventDefault();
3051
+ if (action === "undo") k.undo();
3052
+ else k.redo();
3053
+ };
3054
+ window.addEventListener("keydown", handler);
3055
+ return () => window.removeEventListener("keydown", handler);
3056
+ }, [isActive, enabled]);
3057
+ const value = useMemo(() => ({
3058
+ register,
3059
+ unregister,
3060
+ record,
3061
+ undo,
3062
+ redo,
3063
+ clear,
3064
+ baseline,
3065
+ canUndo,
3066
+ canRedo,
3067
+ enabled,
3068
+ undoLabel: canUndo ? state.past[state.past.length - 1].label : null,
3069
+ redoLabel: canRedo ? state.future[0].label : null
3070
+ }), [register, unregister, record, undo, redo, clear, baseline, canUndo, canRedo, enabled, state.past, state.future]);
3071
+ return /* @__PURE__ */ jsx(UndoContext.Provider, { value, children });
3072
+ }
3073
+ function useUndo() {
3074
+ const ctx = useContext(UndoContext);
3075
+ const noop = useCallback(() => {
3076
+ }, []);
3077
+ return {
3078
+ undo: ctx?.undo ?? noop,
3079
+ redo: ctx?.redo ?? noop,
3080
+ clear: ctx?.clear ?? noop,
3081
+ baseline: ctx?.baseline ?? noop,
3082
+ canUndo: ctx?.canUndo ?? false,
3083
+ canRedo: ctx?.canRedo ?? false,
3084
+ undoLabel: ctx?.undoLabel ?? null,
3085
+ redoLabel: ctx?.redoLabel ?? null,
3086
+ enabled: ctx?.enabled ?? false
3409
3087
  };
3088
+ }
3089
+ function useUndoable(value, apply, opts) {
3090
+ const ctx = useContext(UndoContext);
3091
+ const id = useId();
3092
+ const last = useRef(value);
3093
+ const applyRef = useRef(apply);
3094
+ applyRef.current = apply;
3410
3095
  useEffect(() => {
3411
- if (!open && !dOpen && !pOpen) return;
3412
- return registerModalEscapeInterceptor(() => {
3413
- if (pOpen) {
3414
- handlePClose(false);
3415
- return true;
3416
- }
3417
- if (dOpen) {
3418
- handleDClose(false);
3419
- return true;
3420
- }
3421
- if (open) {
3422
- handleClose(confirmId, false);
3423
- return true;
3424
- }
3425
- return false;
3096
+ if (!ctx) return;
3097
+ ctx.register(id, {
3098
+ getLast: () => last.current,
3099
+ setLast: (v) => {
3100
+ last.current = v;
3101
+ },
3102
+ apply: (v) => applyRef.current(v)
3426
3103
  });
3427
- }, [open, dOpen, pOpen, confirmId]);
3428
- const variant = options.variant || (options.confirmLabel?.toLowerCase().includes("delete") || options.message.toLowerCase().includes("delete") ? "danger" : "info");
3429
- const confirmBtnClass = variant === "danger" ? "bg-red-600 hover:bg-red-700 text-white" : variant === "warning" ? "bg-yellow-600 hover:bg-yellow-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white";
3430
- const iconClass = variant === "danger" ? "text-red-600 bg-red-100" : variant === "warning" ? "text-yellow-600 bg-yellow-100" : "text-blue-600 bg-blue-100";
3431
- return /* @__PURE__ */ jsxs(ConfirmContext.Provider, { value: confirmFn, children: [
3432
- children,
3433
- /* @__PURE__ */ jsxs(Dialog, { open, onClose: () => handleClose(confirmId, false), className: "relative z-[9999]", children: [
3434
- /* @__PURE__ */ jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/30 transition-opacity" }),
3435
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(DialogPanel, { className: "w-full max-w-md rounded-lg bg-white p-6 shadow-xl", children: [
3436
- /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
3437
- /* @__PURE__ */ jsx("div", { className: `shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${iconClass}`, children: /* @__PURE__ */ jsx(ExclamationTriangleIcon, { className: "h-5 w-5" }) }),
3438
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3439
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-base font-semibold text-gray-900", children: options.title || "Confirm" }),
3440
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-gray-600", children: options.message })
3441
- ] })
3442
- ] }),
3443
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
3444
- /* @__PURE__ */ jsx(
3445
- "button",
3446
- {
3447
- type: "button",
3448
- onClick: () => handleClose(confirmId, false),
3449
- className: "bg-white text-gray-700 border border-gray-300 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50",
3450
- children: options.cancelLabel || "Cancel"
3451
- }
3452
- ),
3453
- /* @__PURE__ */ jsx(
3454
- "button",
3455
- {
3456
- type: "button",
3457
- onClick: () => handleClose(confirmId, true),
3458
- className: `px-4 py-2 text-sm font-medium rounded-lg ${confirmBtnClass}`,
3459
- children: options.confirmLabel || "OK"
3460
- }
3461
- )
3462
- ] })
3463
- ] }) })
3464
- ] }),
3465
- /* @__PURE__ */ jsxs(Dialog, { open: dOpen, onClose: () => handleDClose(false), className: "relative z-[9999]", children: [
3466
- /* @__PURE__ */ jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/30 transition-opacity" }),
3467
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(DialogPanel, { className: "relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl", children: [
3468
- /* @__PURE__ */ jsx(
3469
- "button",
3470
- {
3471
- type: "button",
3472
- onClick: () => handleDClose(false),
3473
- "aria-label": "Close",
3474
- className: "absolute right-4 top-4 rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600",
3475
- children: /* @__PURE__ */ jsx(XMarkIcon, { className: "h-5 w-5" })
3476
- }
3477
- ),
3478
- /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
3479
- /* @__PURE__ */ jsx("div", { className: `shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${dOptions.variant === "warning" ? "text-yellow-600 bg-yellow-100" : "text-red-600 bg-red-100"}`, children: /* @__PURE__ */ jsx(ExclamationTriangleIcon, { className: "h-5 w-5" }) }),
3480
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3481
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-base font-semibold text-gray-900 pr-6", children: dOptions.title || "Confirm Action" }),
3482
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-gray-600", children: dOptions.message }),
3483
- /* @__PURE__ */ jsxs("p", { className: "mt-3 text-sm text-gray-700", children: [
3484
- "Type ",
3485
- /* @__PURE__ */ jsx("kbd", { className: "rounded border border-gray-300 bg-gray-50 px-1.5 py-0.5 text-xs font-bold text-red-600 font-mono", children: dOptions.confirmWord }),
3486
- " to confirm:"
3487
- ] }),
3488
- /* @__PURE__ */ jsx(
3489
- "input",
3490
- {
3491
- autoFocus: true,
3492
- type: "text",
3493
- value: dInput,
3494
- onChange: (e) => setDInput(e.target.value),
3495
- onKeyDown: (e) => {
3496
- if (e.key === "Enter" && dInput === dOptions.confirmWord) handleDClose(true);
3497
- },
3498
- placeholder: dOptions.confirmWord,
3499
- className: "mt-2 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-red-500 focus:ring-red-500"
3500
- }
3501
- )
3502
- ] })
3503
- ] }),
3504
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
3505
- /* @__PURE__ */ jsx(
3506
- "button",
3507
- {
3508
- type: "button",
3509
- onClick: () => handleDClose(false),
3510
- className: "bg-white text-gray-700 border border-gray-300 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50",
3511
- children: "Dismiss"
3512
- }
3513
- ),
3514
- /* @__PURE__ */ jsx(
3515
- "button",
3516
- {
3517
- type: "button",
3518
- onClick: () => handleDClose(true),
3519
- disabled: dInput !== dOptions.confirmWord,
3520
- className: `px-4 py-2 text-sm font-medium rounded-lg disabled:opacity-40 ${dOptions.variant === "warning" ? "bg-yellow-600 hover:bg-yellow-700 text-white" : "bg-red-600 hover:bg-red-700 text-white"}`,
3521
- children: dOptions.confirmWord
3522
- }
3523
- )
3524
- ] })
3525
- ] }) })
3526
- ] }),
3527
- /* @__PURE__ */ jsxs(Dialog, { open: pOpen, onClose: () => handlePClose(false), className: "relative z-[9999]", children: [
3528
- /* @__PURE__ */ jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/30 transition-opacity" }),
3529
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(DialogPanel, { className: "w-full max-w-md rounded-lg bg-white p-6 shadow-xl", children: [
3530
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-base font-semibold text-gray-900", children: pOptions.title || "Enter a value" }),
3531
- pOptions.message && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-gray-600", children: pOptions.message }),
3532
- /* @__PURE__ */ jsx(
3533
- "input",
3534
- {
3535
- autoFocus: true,
3536
- type: "text",
3537
- value: pInput,
3538
- onChange: (e) => setPInput(e.target.value),
3539
- onKeyDown: (e) => {
3540
- if (e.key === "Enter") handlePClose(true);
3541
- else if (e.key === "Escape") handlePClose(false);
3542
- },
3543
- onFocus: (e) => e.target.select(),
3544
- placeholder: pOptions.placeholder,
3545
- className: "mt-3 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
3546
- }
3547
- ),
3548
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
3549
- /* @__PURE__ */ jsx(
3550
- "button",
3551
- {
3552
- type: "button",
3553
- onClick: () => handlePClose(false),
3554
- className: "bg-white text-gray-700 border border-gray-300 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50",
3555
- children: pOptions.cancelLabel || "Cancel"
3556
- }
3557
- ),
3558
- /* @__PURE__ */ jsx(
3559
- "button",
3560
- {
3561
- type: "button",
3562
- onClick: () => handlePClose(true),
3563
- disabled: !pOptions.allowEmpty && !pInput.trim(),
3564
- className: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 text-sm font-medium rounded-lg disabled:opacity-40",
3565
- children: pOptions.confirmLabel || "OK"
3566
- }
3567
- )
3568
- ] })
3569
- ] }) })
3570
- ] })
3571
- ] });
3104
+ return () => ctx.unregister(id);
3105
+ }, [ctx, id]);
3106
+ const { label, coalesceKey = null } = opts;
3107
+ const ctxRef = useRef(ctx);
3108
+ ctxRef.current = ctx;
3109
+ const optsRef = useRef({ label, coalesceKey });
3110
+ optsRef.current = { label, coalesceKey };
3111
+ useEffect(() => {
3112
+ const c = ctxRef.current;
3113
+ if (!c) return;
3114
+ if (Object.is(value, last.current)) return;
3115
+ c.record(optsRef.current.label, optsRef.current.coalesceKey);
3116
+ last.current = value;
3117
+ }, [value]);
3118
+ }
3119
+ function useUndoableState(initial, opts) {
3120
+ const [value, setValue] = useState(initial);
3121
+ useUndoable(value, (next) => setValue(() => next), opts);
3122
+ return [value, setValue];
3572
3123
  }
3573
3124
 
3574
- export { CancelButton, ConfirmProvider, CopyButton, DocFavStar, LoadingSpinner, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, SIDEBAR_STRIP_W, ShellPrefsProvider, UndoProvider, WINDOW_REGISTRY, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, beginWindowGesture, cancelWindowFront, client_default, commitExposeHighlight, confirm, confirmDestructive, drainPerfEvents, exitExposeMode, forgetMaximizedWindowBoxes, getActiveModalId, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, isShellApiClientConfigured, markMenuOpen, prompt, registerModalEscapeInterceptor, requestWindowFront, setExposeHighlight, setPerfCollecting, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useConfirm, useIsActiveWindow, useIsMobile, useLocalStoragePrefs, useModalActive, useShellPrefs, useUndo, useUndoable, useUndoableState, useWidgetSettings, useWindowDirty, useWindowManager, useWindowMenuItem, useWindowTitle };
3575
- //# sourceMappingURL=chunk-CVVS2OJO.js.map
3576
- //# sourceMappingURL=chunk-CVVS2OJO.js.map
3125
+ export { CancelButton, CopyButton, DocFavStar, LoadingSpinner, Modal, ModalActions, SIDEBAR_STRIP_W, UndoProvider, WINDOW_REGISTRY, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, beginWindowGesture, cancelWindowFront, client_default, commitExposeHighlight, drainPerfEvents, exitExposeMode, forgetMaximizedWindowBoxes, getActiveModalId, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, isShellApiClientConfigured, markMenuOpen, requestWindowFront, setExposeHighlight, setPerfCollecting, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useIsActiveWindow, useModalActive, useUndo, useUndoable, useUndoableState, useWidgetSettings, useWindowDirty, useWindowManager, useWindowMenuItem, useWindowTitle };
3126
+ //# sourceMappingURL=chunk-IGPSRRL7.js.map
3127
+ //# sourceMappingURL=chunk-IGPSRRL7.js.map