@backbay/glia-desktop 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +37 -0
  2. package/src/components/GliaErrorBoundary/GliaErrorBoundary.tsx +202 -0
  3. package/src/components/GliaErrorBoundary/index.ts +2 -0
  4. package/src/components/GliaErrorBoundary/useErrorBoundary.tsx +61 -0
  5. package/src/components/desktop/Desktop.tsx +204 -0
  6. package/src/components/desktop/DesktopIcon.tsx +293 -0
  7. package/src/components/desktop/FileBrowser.stories.tsx +287 -0
  8. package/src/components/desktop/FileBrowser.tsx +981 -0
  9. package/src/components/desktop/SnapZoneOverlay.tsx +230 -0
  10. package/src/components/desktop/index.ts +15 -0
  11. package/src/components/index.ts +16 -0
  12. package/src/components/shell/Clock.tsx +212 -0
  13. package/src/components/shell/ContextMenu.tsx +249 -0
  14. package/src/components/shell/GlassMenubar.stories.tsx +382 -0
  15. package/src/components/shell/GlassMenubar.tsx +632 -0
  16. package/src/components/shell/NotificationCenter.stories.tsx +515 -0
  17. package/src/components/shell/NotificationCenter.tsx +545 -0
  18. package/src/components/shell/NotificationToast.tsx +319 -0
  19. package/src/components/shell/StartMenu.stories.tsx +249 -0
  20. package/src/components/shell/StartMenu.tsx +568 -0
  21. package/src/components/shell/SystemTray.stories.tsx +492 -0
  22. package/src/components/shell/SystemTray.tsx +457 -0
  23. package/src/components/shell/Taskbar.tsx +387 -0
  24. package/src/components/shell/TaskbarButton.tsx +208 -0
  25. package/src/components/shell/index.ts +37 -0
  26. package/src/components/window/Window.tsx +751 -0
  27. package/src/components/window/WindowTitlebar.tsx +359 -0
  28. package/src/components/window/index.ts +10 -0
  29. package/src/core/desktop/fileBrowserTypes.ts +112 -0
  30. package/src/core/desktop/index.ts +8 -0
  31. package/src/core/desktop/types.ts +185 -0
  32. package/src/core/desktop/useFileBrowser.tsx +405 -0
  33. package/src/core/desktop/useSnapZones.tsx +203 -0
  34. package/src/core/index.ts +11 -0
  35. package/src/core/shell/__tests__/useNotifications.test.ts +155 -0
  36. package/src/core/shell/__tests__/useTaskbar.test.ts +99 -0
  37. package/src/core/shell/index.ts +10 -0
  38. package/src/core/shell/notificationTypes.ts +110 -0
  39. package/src/core/shell/types.ts +194 -0
  40. package/src/core/shell/useNotifications.tsx +259 -0
  41. package/src/core/shell/useStartMenu.tsx +242 -0
  42. package/src/core/shell/useSystemTray.tsx +175 -0
  43. package/src/core/shell/useTaskbar.tsx +320 -0
  44. package/src/core/useKeyboardNavigation.ts +41 -0
  45. package/src/core/window/__tests__/useWindowManager.test.ts +269 -0
  46. package/src/core/window/index.ts +6 -0
  47. package/src/core/window/types.ts +149 -0
  48. package/src/core/window/useWindowManager.tsx +1154 -0
  49. package/src/index.ts +146 -0
  50. package/src/lib/utils.ts +6 -0
  51. package/src/providers/DesktopOSProvider.tsx +391 -0
  52. package/src/providers/ThemeProvider.tsx +162 -0
  53. package/src/providers/index.ts +6 -0
  54. package/src/themes/default.ts +107 -0
  55. package/src/themes/index.ts +6 -0
  56. package/src/themes/types.ts +230 -0
  57. package/tsconfig.json +20 -0
  58. package/tsup.config.ts +16 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - useTaskbar Hook
3
+ *
4
+ * Taskbar state management for button positions and hover previews.
5
+ * Used for minimize/restore animations and window preview tooltips.
6
+ */
7
+
8
+ import { useMemo, createContext, useContext, useState, type ReactNode } from 'react';
9
+ import { create, createStore, useStore } from 'zustand';
10
+ import { useShallow } from 'zustand/react/shallow';
11
+ import type { WindowId } from '../window/types';
12
+ import type {
13
+ TaskbarButtonPosition,
14
+ TaskbarPreviewState,
15
+ TaskbarItem,
16
+ UseTaskbarReturn,
17
+ } from './types';
18
+
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ // Constants
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+
23
+ /** Delay before showing preview (ms) */
24
+ const PREVIEW_SHOW_DELAY = 200;
25
+
26
+ /** Delay before hiding preview - allows moving to preview (ms) */
27
+ const PREVIEW_HIDE_DELAY = 100;
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ // Store Interface
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+
33
+ interface TaskbarStore {
34
+ buttonPositions: Map<string, TaskbarButtonPosition>;
35
+ previewState: TaskbarPreviewState | null;
36
+ pinnedApps: string[];
37
+ _showPreviewTimeout: ReturnType<typeof setTimeout> | null;
38
+ _hidePreviewTimeout: ReturnType<typeof setTimeout> | null;
39
+
40
+ registerButtonPosition: (id: string, position: TaskbarButtonPosition) => void;
41
+ unregisterButtonPosition: (id: string) => void;
42
+ getButtonPosition: (id: string) => TaskbarButtonPosition | undefined;
43
+ getButtonCenter: (id: string) => { x: number; y: number } | undefined;
44
+
45
+ startPreviewHover: (windowId: WindowId, icon?: string) => void;
46
+ endPreviewHover: () => void;
47
+ cancelHidePreview: () => void;
48
+ hidePreview: () => void;
49
+
50
+ pinApp: (appId: string) => void;
51
+ unpinApp: (appId: string) => void;
52
+ }
53
+
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+ // Store Factory
56
+ // ═══════════════════════════════════════════════════════════════════════════
57
+
58
+ function createTaskbarStoreImpl(set: any, get: any): TaskbarStore {
59
+ return {
60
+ buttonPositions: new Map(),
61
+ previewState: null,
62
+ pinnedApps: [],
63
+ _showPreviewTimeout: null,
64
+ _hidePreviewTimeout: null,
65
+
66
+ registerButtonPosition: (id, position) => {
67
+ set((state: TaskbarStore) => {
68
+ const newPositions = new Map(state.buttonPositions);
69
+ newPositions.set(id, position);
70
+ return { buttonPositions: newPositions };
71
+ });
72
+ },
73
+
74
+ unregisterButtonPosition: (id) => {
75
+ set((state: TaskbarStore) => {
76
+ const newPositions = new Map(state.buttonPositions);
77
+ newPositions.delete(id);
78
+ return { buttonPositions: newPositions };
79
+ });
80
+ },
81
+
82
+ getButtonPosition: (id) => {
83
+ return get().buttonPositions.get(id);
84
+ },
85
+
86
+ getButtonCenter: (id) => {
87
+ const pos = get().buttonPositions.get(id);
88
+ if (!pos) return undefined;
89
+ return {
90
+ x: pos.x + pos.width / 2,
91
+ y: pos.y + pos.height / 2,
92
+ };
93
+ },
94
+
95
+ startPreviewHover: (windowId, icon) => {
96
+ const state = get();
97
+
98
+ if (state._hidePreviewTimeout) {
99
+ clearTimeout(state._hidePreviewTimeout);
100
+ set({ _hidePreviewTimeout: null });
101
+ }
102
+
103
+ if (state._showPreviewTimeout) {
104
+ clearTimeout(state._showPreviewTimeout);
105
+ }
106
+
107
+ const timeout = setTimeout(() => {
108
+ const currentState = get();
109
+ const anchorRect = currentState.buttonPositions.get(windowId);
110
+ if (anchorRect) {
111
+ set({
112
+ previewState: { windowId, anchorRect, icon },
113
+ _showPreviewTimeout: null,
114
+ });
115
+ }
116
+ }, PREVIEW_SHOW_DELAY);
117
+
118
+ set({ _showPreviewTimeout: timeout });
119
+ },
120
+
121
+ endPreviewHover: () => {
122
+ const state = get();
123
+
124
+ if (state._showPreviewTimeout) {
125
+ clearTimeout(state._showPreviewTimeout);
126
+ set({ _showPreviewTimeout: null });
127
+ }
128
+
129
+ if (state._hidePreviewTimeout) {
130
+ clearTimeout(state._hidePreviewTimeout);
131
+ }
132
+
133
+ const timeout = setTimeout(() => {
134
+ set({ previewState: null, _hidePreviewTimeout: null });
135
+ }, PREVIEW_HIDE_DELAY);
136
+
137
+ set({ _hidePreviewTimeout: timeout });
138
+ },
139
+
140
+ cancelHidePreview: () => {
141
+ const state = get();
142
+
143
+ if (state._hidePreviewTimeout) {
144
+ clearTimeout(state._hidePreviewTimeout);
145
+ set({ _hidePreviewTimeout: null });
146
+ }
147
+ },
148
+
149
+ hidePreview: () => {
150
+ const state = get();
151
+
152
+ if (state._showPreviewTimeout) {
153
+ clearTimeout(state._showPreviewTimeout);
154
+ }
155
+ if (state._hidePreviewTimeout) {
156
+ clearTimeout(state._hidePreviewTimeout);
157
+ }
158
+
159
+ set({
160
+ previewState: null,
161
+ _showPreviewTimeout: null,
162
+ _hidePreviewTimeout: null,
163
+ });
164
+ },
165
+
166
+ pinApp: (appId) => {
167
+ set((state: TaskbarStore) => ({
168
+ pinnedApps: state.pinnedApps.includes(appId)
169
+ ? state.pinnedApps
170
+ : [...state.pinnedApps, appId],
171
+ }));
172
+ },
173
+
174
+ unpinApp: (appId) => {
175
+ set((state: TaskbarStore) => ({
176
+ pinnedApps: state.pinnedApps.filter((id: string) => id !== appId),
177
+ }));
178
+ },
179
+ };
180
+ }
181
+
182
+ /** Factory: creates an isolated Taskbar store instance. */
183
+ export function createTaskbarStore() {
184
+ return createStore<TaskbarStore>((set, get) => createTaskbarStoreImpl(set, get));
185
+ }
186
+
187
+ export type TaskbarStoreApi = ReturnType<typeof createTaskbarStore>;
188
+
189
+ const TaskbarStoreContext = createContext<TaskbarStoreApi | null>(null);
190
+
191
+ /** Provider that creates an isolated Taskbar store for its subtree. */
192
+ export function TaskbarStoreProvider({ children }: { children: ReactNode }) {
193
+ const [store] = useState(() => createTaskbarStore());
194
+ return (
195
+ <TaskbarStoreContext.Provider value={store}>
196
+ {children}
197
+ </TaskbarStoreContext.Provider>
198
+ );
199
+ }
200
+
201
+ // Legacy singleton
202
+ export const useTaskbarStore = create<TaskbarStore>((set, get) => createTaskbarStoreImpl(set, get));
203
+
204
+ // ═══════════════════════════════════════════════════════════════════════════
205
+ // Context-aware store resolver
206
+ // ═══════════════════════════════════════════════════════════════════════════
207
+
208
+ /** Returns the context-provided store if available, otherwise the singleton. */
209
+ function useResolvedTaskbarStore(): import('zustand').StoreApi<TaskbarStore> {
210
+ const contextStore = useContext(TaskbarStoreContext);
211
+ return contextStore ?? useTaskbarStore;
212
+ }
213
+
214
+ // ═══════════════════════════════════════════════════════════════════════════
215
+ // Public Hook
216
+ // ═══════════════════════════════════════════════════════════════════════════
217
+
218
+ /**
219
+ * Hook for taskbar state management.
220
+ *
221
+ * Handles button position tracking for animations and hover preview state.
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * const { registerButtonPosition, startPreviewHover, endPreviewHover } = useTaskbar();
226
+ *
227
+ * // Register button position on mount
228
+ * useEffect(() => {
229
+ * const rect = buttonRef.current?.getBoundingClientRect();
230
+ * if (rect) {
231
+ * registerButtonPosition(windowId, {
232
+ * x: rect.x, y: rect.y, width: rect.width, height: rect.height
233
+ * });
234
+ * }
235
+ * }, []);
236
+ *
237
+ * // Handle hover
238
+ * <button
239
+ * onMouseEnter={() => startPreviewHover(windowId)}
240
+ * onMouseLeave={endPreviewHover}
241
+ * >
242
+ * {title}
243
+ * </button>
244
+ * ```
245
+ */
246
+ export function useTaskbar(
247
+ windows: Array<{ id: string; appId: string; title: string; icon?: string }> = [],
248
+ activeWindowId: WindowId | null = null
249
+ ): UseTaskbarReturn {
250
+ const resolvedStore = useResolvedTaskbarStore();
251
+ const store = useStore(
252
+ resolvedStore,
253
+ useShallow((state) => ({
254
+ buttonPositions: state.buttonPositions,
255
+ previewState: state.previewState,
256
+ pinnedApps: state.pinnedApps,
257
+ registerButtonPosition: state.registerButtonPosition,
258
+ unregisterButtonPosition: state.unregisterButtonPosition,
259
+ getButtonCenter: state.getButtonCenter,
260
+ startPreviewHover: state.startPreviewHover,
261
+ endPreviewHover: state.endPreviewHover,
262
+ cancelHidePreview: state.cancelHidePreview,
263
+ hidePreview: state.hidePreview,
264
+ pinApp: state.pinApp,
265
+ unpinApp: state.unpinApp,
266
+ })),
267
+ );
268
+
269
+ // Build taskbar items from windows and pinned apps (memoized to prevent unnecessary re-renders)
270
+ const items = useMemo(() => {
271
+ const result: TaskbarItem[] = [];
272
+
273
+ // Add pinned apps (even if not running)
274
+ for (const appId of store.pinnedApps) {
275
+ const runningWindow = windows.find((w) => w.appId === appId);
276
+ result.push({
277
+ id: runningWindow?.id ?? `pinned-${appId}`,
278
+ windowId: runningWindow?.id,
279
+ appId,
280
+ title: runningWindow?.title ?? appId,
281
+ icon: runningWindow?.icon,
282
+ isPinned: true,
283
+ isActive: runningWindow?.id === activeWindowId,
284
+ });
285
+ }
286
+
287
+ // Add running windows that aren't pinned
288
+ for (const window of windows) {
289
+ if (!store.pinnedApps.includes(window.appId)) {
290
+ result.push({
291
+ id: window.id,
292
+ windowId: window.id,
293
+ appId: window.appId,
294
+ title: window.title,
295
+ icon: window.icon,
296
+ isPinned: false,
297
+ isActive: window.id === activeWindowId,
298
+ });
299
+ }
300
+ }
301
+
302
+ return result;
303
+ }, [windows, store.pinnedApps, activeWindowId]);
304
+
305
+ return {
306
+ items,
307
+ activeWindowId,
308
+ previewState: store.previewState,
309
+ buttonPositions: store.buttonPositions,
310
+ registerButtonPosition: store.registerButtonPosition,
311
+ unregisterButtonPosition: store.unregisterButtonPosition,
312
+ getButtonCenter: store.getButtonCenter,
313
+ pin: store.pinApp,
314
+ unpin: store.unpinApp,
315
+ startPreviewHover: store.startPreviewHover,
316
+ endPreviewHover: store.endPreviewHover,
317
+ cancelHidePreview: store.cancelHidePreview,
318
+ hidePreview: store.hidePreview,
319
+ };
320
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useWindowManagerStore, type WindowManagerStoreApi } from "./window/useWindowManager";
5
+
6
+ /**
7
+ * Global keyboard navigation hook for the desktop shell.
8
+ *
9
+ * Listens for:
10
+ * - Ctrl+Tab: Cycle focus to the next open window
11
+ * - Escape: Close the current overlay / exit fullscreen
12
+ *
13
+ * Call this once from the desktop shell root component.
14
+ *
15
+ * @param storeApi - Optional store API; defaults to the legacy singleton.
16
+ */
17
+ export function useKeyboardNavigation(storeApi?: WindowManagerStoreApi) {
18
+ useEffect(() => {
19
+ const api = storeApi ?? useWindowManagerStore;
20
+
21
+ function handleKeyDown(e: KeyboardEvent) {
22
+ const store = api.getState();
23
+
24
+ // Ctrl+Tab — cycle to next window
25
+ if (e.key === "Tab" && e.ctrlKey && !e.altKey && !e.metaKey) {
26
+ e.preventDefault();
27
+ store._cycleFocusNext();
28
+ return;
29
+ }
30
+
31
+ // Escape — exit fullscreen if active
32
+ if (e.key === "Escape" && store.fullscreenId) {
33
+ store._exitFullscreen();
34
+ return;
35
+ }
36
+ }
37
+
38
+ globalThis.addEventListener("keydown", handleKeyDown);
39
+ return () => globalThis.removeEventListener("keydown", handleKeyDown);
40
+ }, [storeApi]);
41
+ }
@@ -0,0 +1,269 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { useWindowManagerStore } from '../useWindowManager';
3
+
4
+ function resetStore() {
5
+ useWindowManagerStore.setState({
6
+ windows: new Map(),
7
+ groups: new Map(),
8
+ focusedId: null,
9
+ fullscreenId: null,
10
+ nextZIndex: 1,
11
+ });
12
+ }
13
+
14
+ describe('WindowManager store', () => {
15
+ beforeEach(() => {
16
+ resetStore();
17
+ });
18
+
19
+ describe('open', () => {
20
+ it('creates a window entry', () => {
21
+ const store = useWindowManagerStore.getState();
22
+ const id = store._open({ title: 'Test Window' });
23
+
24
+ const state = useWindowManagerStore.getState();
25
+ expect(state.windows.size).toBe(1);
26
+ expect(state.windows.get(id)).toBeDefined();
27
+ expect(state.windows.get(id)!.title).toBe('Test Window');
28
+ });
29
+
30
+ it('sets the new window as focused', () => {
31
+ const store = useWindowManagerStore.getState();
32
+ const id = store._open({ title: 'Test' });
33
+
34
+ const state = useWindowManagerStore.getState();
35
+ expect(state.focusedId).toBe(id);
36
+ expect(state.windows.get(id)!.isFocused).toBe(true);
37
+ });
38
+
39
+ it('uses default size when none provided', () => {
40
+ const store = useWindowManagerStore.getState();
41
+ const id = store._open({ title: 'Test' });
42
+
43
+ const w = useWindowManagerStore.getState().windows.get(id)!;
44
+ expect(w.size).toEqual({ width: 640, height: 480 });
45
+ });
46
+
47
+ it('uses provided size', () => {
48
+ const store = useWindowManagerStore.getState();
49
+ const id = store._open({ title: 'Test', size: { width: 800, height: 600 } });
50
+
51
+ const w = useWindowManagerStore.getState().windows.get(id)!;
52
+ expect(w.size).toEqual({ width: 800, height: 600 });
53
+ });
54
+
55
+ it('increments z-index for each new window', () => {
56
+ const store = useWindowManagerStore.getState();
57
+ const id1 = store._open({ title: 'W1' });
58
+ const id2 = useWindowManagerStore.getState()._open({ title: 'W2' });
59
+
60
+ const state = useWindowManagerStore.getState();
61
+ const w1 = state.windows.get(id1)!;
62
+ const w2 = state.windows.get(id2)!;
63
+ expect(w2.zIndex).toBeGreaterThan(w1.zIndex);
64
+ });
65
+ });
66
+
67
+ describe('close', () => {
68
+ it('removes the window', () => {
69
+ const store = useWindowManagerStore.getState();
70
+ const id = store._open({ title: 'Test' });
71
+ expect(useWindowManagerStore.getState().windows.size).toBe(1);
72
+
73
+ useWindowManagerStore.getState()._close(id);
74
+ expect(useWindowManagerStore.getState().windows.size).toBe(0);
75
+ });
76
+
77
+ it('focuses next topmost window after closing focused window', () => {
78
+ const store = useWindowManagerStore.getState();
79
+ const id1 = store._open({ title: 'W1' });
80
+ const id2 = useWindowManagerStore.getState()._open({ title: 'W2' });
81
+
82
+ expect(useWindowManagerStore.getState().focusedId).toBe(id2);
83
+
84
+ useWindowManagerStore.getState()._close(id2);
85
+ expect(useWindowManagerStore.getState().focusedId).toBe(id1);
86
+ });
87
+
88
+ it('sets focusedId to null when last window is closed', () => {
89
+ const store = useWindowManagerStore.getState();
90
+ const id = store._open({ title: 'Test' });
91
+
92
+ useWindowManagerStore.getState()._close(id);
93
+ expect(useWindowManagerStore.getState().focusedId).toBeNull();
94
+ });
95
+
96
+ it('does nothing for non-existent window', () => {
97
+ const store = useWindowManagerStore.getState();
98
+ store._open({ title: 'Test' });
99
+
100
+ useWindowManagerStore.getState()._close('non-existent');
101
+ expect(useWindowManagerStore.getState().windows.size).toBe(1);
102
+ });
103
+ });
104
+
105
+ describe('focus', () => {
106
+ it('updates z-index and focused state', () => {
107
+ const store = useWindowManagerStore.getState();
108
+ const id1 = store._open({ title: 'W1' });
109
+ const id2 = useWindowManagerStore.getState()._open({ title: 'W2' });
110
+
111
+ // id2 is focused, focus id1
112
+ useWindowManagerStore.getState()._focus(id1);
113
+
114
+ const state = useWindowManagerStore.getState();
115
+ expect(state.focusedId).toBe(id1);
116
+ expect(state.windows.get(id1)!.isFocused).toBe(true);
117
+ expect(state.windows.get(id2)!.isFocused).toBe(false);
118
+ });
119
+
120
+ it('does nothing if window is already focused', () => {
121
+ const store = useWindowManagerStore.getState();
122
+ const id = store._open({ title: 'Test' });
123
+
124
+ const zBefore = useWindowManagerStore.getState().nextZIndex;
125
+ useWindowManagerStore.getState()._focus(id);
126
+ const zAfter = useWindowManagerStore.getState().nextZIndex;
127
+
128
+ // nextZIndex should not change since it was already focused
129
+ expect(zAfter).toBe(zBefore);
130
+ });
131
+ });
132
+
133
+ describe('minimize', () => {
134
+ it('sets isMinimized to true', () => {
135
+ const store = useWindowManagerStore.getState();
136
+ const id = store._open({ title: 'Test' });
137
+
138
+ useWindowManagerStore.getState()._minimize(id);
139
+
140
+ const w = useWindowManagerStore.getState().windows.get(id)!;
141
+ expect(w.isMinimized).toBe(true);
142
+ });
143
+
144
+ it('focuses next topmost non-minimized window', () => {
145
+ const store = useWindowManagerStore.getState();
146
+ const id1 = store._open({ title: 'W1' });
147
+ const id2 = useWindowManagerStore.getState()._open({ title: 'W2' });
148
+
149
+ useWindowManagerStore.getState()._minimize(id2);
150
+
151
+ expect(useWindowManagerStore.getState().focusedId).toBe(id1);
152
+ });
153
+
154
+ it('sets focusedId to null when all windows are minimized', () => {
155
+ const store = useWindowManagerStore.getState();
156
+ const id = store._open({ title: 'Test' });
157
+
158
+ useWindowManagerStore.getState()._minimize(id);
159
+
160
+ expect(useWindowManagerStore.getState().focusedId).toBeNull();
161
+ });
162
+ });
163
+
164
+ describe('maximize', () => {
165
+ it('sets isMaximized to true and saves pre-maximize state', () => {
166
+ const store = useWindowManagerStore.getState();
167
+ const id = store._open({ title: 'Test', position: { x: 100, y: 100 }, size: { width: 400, height: 300 } });
168
+
169
+ useWindowManagerStore.getState()._maximize(id);
170
+
171
+ const w = useWindowManagerStore.getState().windows.get(id)!;
172
+ expect(w.isMaximized).toBe(true);
173
+ expect(w.preMaximize).toEqual({ x: 100, y: 100, width: 400, height: 300 });
174
+ expect(w.position).toEqual({ x: 0, y: 0 });
175
+ });
176
+
177
+ it('does nothing if already maximized', () => {
178
+ const store = useWindowManagerStore.getState();
179
+ const id = store._open({ title: 'Test' });
180
+
181
+ useWindowManagerStore.getState()._maximize(id);
182
+ const firstSize = useWindowManagerStore.getState().windows.get(id)!.size;
183
+
184
+ useWindowManagerStore.getState()._maximize(id);
185
+ const secondSize = useWindowManagerStore.getState().windows.get(id)!.size;
186
+
187
+ expect(firstSize).toEqual(secondSize);
188
+ });
189
+ });
190
+
191
+ describe('restore', () => {
192
+ it('restores from minimized', () => {
193
+ const store = useWindowManagerStore.getState();
194
+ const id = store._open({ title: 'Test' });
195
+
196
+ useWindowManagerStore.getState()._minimize(id);
197
+ expect(useWindowManagerStore.getState().windows.get(id)!.isMinimized).toBe(true);
198
+
199
+ useWindowManagerStore.getState()._restore(id);
200
+
201
+ const w = useWindowManagerStore.getState().windows.get(id)!;
202
+ expect(w.isMinimized).toBe(false);
203
+ });
204
+
205
+ it('restores from maximized to pre-maximize position', () => {
206
+ const store = useWindowManagerStore.getState();
207
+ const id = store._open({ title: 'Test', position: { x: 100, y: 200 }, size: { width: 400, height: 300 } });
208
+
209
+ useWindowManagerStore.getState()._maximize(id);
210
+ useWindowManagerStore.getState()._restore(id);
211
+
212
+ const w = useWindowManagerStore.getState().windows.get(id)!;
213
+ expect(w.isMaximized).toBe(false);
214
+ expect(w.position).toEqual({ x: 100, y: 200 });
215
+ expect(w.size).toEqual({ width: 400, height: 300 });
216
+ });
217
+ });
218
+
219
+ describe('stacking order', () => {
220
+ it('maintains correct z-index order with multiple windows', () => {
221
+ const store = useWindowManagerStore.getState();
222
+ const id1 = store._open({ title: 'W1' });
223
+ const id2 = useWindowManagerStore.getState()._open({ title: 'W2' });
224
+ const id3 = useWindowManagerStore.getState()._open({ title: 'W3' });
225
+
226
+ // Focus id1, bringing it to top
227
+ useWindowManagerStore.getState()._focus(id1);
228
+
229
+ const state = useWindowManagerStore.getState();
230
+ const z1 = state.windows.get(id1)!.zIndex;
231
+ const z2 = state.windows.get(id2)!.zIndex;
232
+ const z3 = state.windows.get(id3)!.zIndex;
233
+
234
+ // id1 should be on top (highest z-index)
235
+ expect(z1).toBeGreaterThan(z2);
236
+ expect(z1).toBeGreaterThan(z3);
237
+ });
238
+ });
239
+
240
+ describe('minimizeAll', () => {
241
+ it('minimizes all windows and clears focus', () => {
242
+ const store = useWindowManagerStore.getState();
243
+ store._open({ title: 'W1' });
244
+ useWindowManagerStore.getState()._open({ title: 'W2' });
245
+
246
+ useWindowManagerStore.getState()._minimizeAll();
247
+
248
+ const state = useWindowManagerStore.getState();
249
+ for (const [, w] of state.windows) {
250
+ expect(w.isMinimized).toBe(true);
251
+ }
252
+ expect(state.focusedId).toBeNull();
253
+ });
254
+ });
255
+
256
+ describe('closeAll', () => {
257
+ it('removes all windows', () => {
258
+ const store = useWindowManagerStore.getState();
259
+ store._open({ title: 'W1' });
260
+ useWindowManagerStore.getState()._open({ title: 'W2' });
261
+
262
+ useWindowManagerStore.getState()._closeAll();
263
+
264
+ const state = useWindowManagerStore.getState();
265
+ expect(state.windows.size).toBe(0);
266
+ expect(state.focusedId).toBeNull();
267
+ });
268
+ });
269
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - Window Core
3
+ */
4
+
5
+ export * from './types';
6
+ export * from './useWindowManager';