@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.
- package/package.json +37 -0
- package/src/components/GliaErrorBoundary/GliaErrorBoundary.tsx +202 -0
- package/src/components/GliaErrorBoundary/index.ts +2 -0
- package/src/components/GliaErrorBoundary/useErrorBoundary.tsx +61 -0
- package/src/components/desktop/Desktop.tsx +204 -0
- package/src/components/desktop/DesktopIcon.tsx +293 -0
- package/src/components/desktop/FileBrowser.stories.tsx +287 -0
- package/src/components/desktop/FileBrowser.tsx +981 -0
- package/src/components/desktop/SnapZoneOverlay.tsx +230 -0
- package/src/components/desktop/index.ts +15 -0
- package/src/components/index.ts +16 -0
- package/src/components/shell/Clock.tsx +212 -0
- package/src/components/shell/ContextMenu.tsx +249 -0
- package/src/components/shell/GlassMenubar.stories.tsx +382 -0
- package/src/components/shell/GlassMenubar.tsx +632 -0
- package/src/components/shell/NotificationCenter.stories.tsx +515 -0
- package/src/components/shell/NotificationCenter.tsx +545 -0
- package/src/components/shell/NotificationToast.tsx +319 -0
- package/src/components/shell/StartMenu.stories.tsx +249 -0
- package/src/components/shell/StartMenu.tsx +568 -0
- package/src/components/shell/SystemTray.stories.tsx +492 -0
- package/src/components/shell/SystemTray.tsx +457 -0
- package/src/components/shell/Taskbar.tsx +387 -0
- package/src/components/shell/TaskbarButton.tsx +208 -0
- package/src/components/shell/index.ts +37 -0
- package/src/components/window/Window.tsx +751 -0
- package/src/components/window/WindowTitlebar.tsx +359 -0
- package/src/components/window/index.ts +10 -0
- package/src/core/desktop/fileBrowserTypes.ts +112 -0
- package/src/core/desktop/index.ts +8 -0
- package/src/core/desktop/types.ts +185 -0
- package/src/core/desktop/useFileBrowser.tsx +405 -0
- package/src/core/desktop/useSnapZones.tsx +203 -0
- package/src/core/index.ts +11 -0
- package/src/core/shell/__tests__/useNotifications.test.ts +155 -0
- package/src/core/shell/__tests__/useTaskbar.test.ts +99 -0
- package/src/core/shell/index.ts +10 -0
- package/src/core/shell/notificationTypes.ts +110 -0
- package/src/core/shell/types.ts +194 -0
- package/src/core/shell/useNotifications.tsx +259 -0
- package/src/core/shell/useStartMenu.tsx +242 -0
- package/src/core/shell/useSystemTray.tsx +175 -0
- package/src/core/shell/useTaskbar.tsx +320 -0
- package/src/core/useKeyboardNavigation.ts +41 -0
- package/src/core/window/__tests__/useWindowManager.test.ts +269 -0
- package/src/core/window/index.ts +6 -0
- package/src/core/window/types.ts +149 -0
- package/src/core/window/useWindowManager.tsx +1154 -0
- package/src/index.ts +146 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/DesktopOSProvider.tsx +391 -0
- package/src/providers/ThemeProvider.tsx +162 -0
- package/src/providers/index.ts +6 -0
- package/src/themes/default.ts +107 -0
- package/src/themes/index.ts +6 -0
- package/src/themes/types.ts +230 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @backbay/glia Desktop OS
|
|
3
|
+
*
|
|
4
|
+
* Reusable desktop OS components for React applications.
|
|
5
|
+
* Includes window management, taskbar, context menus, and more.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage with provider
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { DesktopOSProvider, Window, Taskbar, Desktop } from '@backbay/glia/desktop';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <DesktopOSProvider
|
|
14
|
+
* processes={myApps}
|
|
15
|
+
* theme={{ colors: { accent: '#00ff88' } }}
|
|
16
|
+
* >
|
|
17
|
+
* <Desktop />
|
|
18
|
+
* <Taskbar />
|
|
19
|
+
* </DesktopOSProvider>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example Power user path with headless hooks
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import { useWindowManager, useTaskbar } from '@backbay/glia/desktop/core';
|
|
27
|
+
*
|
|
28
|
+
* function MyWindowManager() {
|
|
29
|
+
* const { windows, open, close, focus } = useWindowManager();
|
|
30
|
+
* // Build your own UI
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// Core (headless hooks and types)
|
|
36
|
+
// Note: DesktopIcon type is exported as DesktopIconDef to avoid conflict with DesktopIcon component
|
|
37
|
+
export {
|
|
38
|
+
// Window types
|
|
39
|
+
type WindowId,
|
|
40
|
+
type TilePosition,
|
|
41
|
+
type WindowState,
|
|
42
|
+
type WindowGroup,
|
|
43
|
+
type WindowOpenConfig,
|
|
44
|
+
type UseWindowManagerReturn,
|
|
45
|
+
// Window hooks
|
|
46
|
+
useWindowManager,
|
|
47
|
+
useWindowManagerStore,
|
|
48
|
+
createWindowManagerStore,
|
|
49
|
+
WindowManagerStoreProvider,
|
|
50
|
+
type WindowManagerStoreApi,
|
|
51
|
+
useWindowIds,
|
|
52
|
+
useWindow,
|
|
53
|
+
useIsWindowFocused,
|
|
54
|
+
useIsWindowFullscreen,
|
|
55
|
+
useIsFullscreenActive,
|
|
56
|
+
useWindowActions,
|
|
57
|
+
useWindowGroup,
|
|
58
|
+
// Shell types
|
|
59
|
+
type TaskbarButtonPosition,
|
|
60
|
+
type TaskbarItem,
|
|
61
|
+
type TaskbarPreviewState,
|
|
62
|
+
type UseTaskbarReturn,
|
|
63
|
+
type ContextMenuTargetType,
|
|
64
|
+
type ContextMenuTarget,
|
|
65
|
+
type ContextMenuItem,
|
|
66
|
+
type UseContextMenuReturn,
|
|
67
|
+
type StartMenuCategory,
|
|
68
|
+
type StartMenuApp,
|
|
69
|
+
type UseStartMenuReturn,
|
|
70
|
+
// Shell hooks
|
|
71
|
+
useTaskbar,
|
|
72
|
+
useTaskbarStore,
|
|
73
|
+
createTaskbarStore,
|
|
74
|
+
TaskbarStoreProvider,
|
|
75
|
+
type TaskbarStoreApi,
|
|
76
|
+
useStartMenu,
|
|
77
|
+
useStartMenuStore,
|
|
78
|
+
createStartMenuStore,
|
|
79
|
+
StartMenuStoreProvider,
|
|
80
|
+
type StartMenuStoreApi,
|
|
81
|
+
useSystemTray,
|
|
82
|
+
useSystemTrayStore,
|
|
83
|
+
createSystemTrayStore,
|
|
84
|
+
SystemTrayStoreProvider,
|
|
85
|
+
type SystemTrayStoreApi,
|
|
86
|
+
type SystemTrayItem as SystemTrayItemDef,
|
|
87
|
+
type UseSystemTrayReturn,
|
|
88
|
+
// Notification types
|
|
89
|
+
type NotificationType,
|
|
90
|
+
type NotificationPriority,
|
|
91
|
+
type NotificationAction,
|
|
92
|
+
type Notification,
|
|
93
|
+
type NotificationGroup,
|
|
94
|
+
type NotificationInput,
|
|
95
|
+
type UseNotificationsReturn,
|
|
96
|
+
// Notification hooks
|
|
97
|
+
useNotifications,
|
|
98
|
+
useNotificationStore,
|
|
99
|
+
createNotificationStore,
|
|
100
|
+
NotificationStoreProvider,
|
|
101
|
+
type NotificationStoreApi,
|
|
102
|
+
// Desktop types
|
|
103
|
+
type ProcessDefinition,
|
|
104
|
+
type ProcessInstance,
|
|
105
|
+
type DesktopIcon as DesktopIconDef,
|
|
106
|
+
type UseDesktopReturn,
|
|
107
|
+
type UseProcessRegistryReturn,
|
|
108
|
+
type SnapZone,
|
|
109
|
+
type UseSnapZonesReturn,
|
|
110
|
+
type DesktopOSConfig,
|
|
111
|
+
// Desktop hooks
|
|
112
|
+
useSnapZones,
|
|
113
|
+
useSnapZoneStore,
|
|
114
|
+
createSnapZoneStore,
|
|
115
|
+
SnapZoneStoreProvider,
|
|
116
|
+
type SnapZoneStoreApi,
|
|
117
|
+
getSnapZoneDimensions,
|
|
118
|
+
EDGE_THRESHOLD,
|
|
119
|
+
CORNER_SIZE,
|
|
120
|
+
// FileBrowser types
|
|
121
|
+
type FileItem,
|
|
122
|
+
type FileType,
|
|
123
|
+
type FileBrowserViewMode,
|
|
124
|
+
type FileBrowserSortField,
|
|
125
|
+
type FileBrowserSortOrder,
|
|
126
|
+
type FileBrowserSort,
|
|
127
|
+
type FileBrowserProps,
|
|
128
|
+
type UseFileBrowserReturn,
|
|
129
|
+
// FileBrowser hooks
|
|
130
|
+
useFileBrowser,
|
|
131
|
+
useFileBrowserStore,
|
|
132
|
+
createFileBrowserStore,
|
|
133
|
+
FileBrowserStoreProvider,
|
|
134
|
+
type FileBrowserStoreApi,
|
|
135
|
+
} from './core';
|
|
136
|
+
|
|
137
|
+
// Themes
|
|
138
|
+
export * from './themes';
|
|
139
|
+
|
|
140
|
+
// Providers
|
|
141
|
+
export * from './providers';
|
|
142
|
+
|
|
143
|
+
// Components (styled)
|
|
144
|
+
export * from './components/desktop';
|
|
145
|
+
export * from './components/window';
|
|
146
|
+
export * from './components/shell';
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @backbay/glia Desktop OS - DesktopOSProvider
|
|
3
|
+
*
|
|
4
|
+
* Main provider that wires together all desktop OS functionality.
|
|
5
|
+
* Manages windows, processes, taskbar, and context menus.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, {
|
|
11
|
+
createContext,
|
|
12
|
+
useContext,
|
|
13
|
+
useMemo,
|
|
14
|
+
useCallback,
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
} from 'react';
|
|
18
|
+
import type { PartialDesktopOSTheme } from '../themes/types';
|
|
19
|
+
import { ThemeProvider } from './ThemeProvider';
|
|
20
|
+
import {
|
|
21
|
+
useWindowManager,
|
|
22
|
+
useWindowManagerStore,
|
|
23
|
+
} from '../core/window/useWindowManager';
|
|
24
|
+
import type { WindowId, WindowState, UseWindowManagerReturn } from '../core/window/types';
|
|
25
|
+
import { useSnapZones } from '../core/desktop/useSnapZones';
|
|
26
|
+
import type { UseSnapZonesReturn } from '../core/desktop/types';
|
|
27
|
+
import { useTaskbar, useTaskbarStore } from '../core/shell/useTaskbar';
|
|
28
|
+
import type { UseTaskbarReturn } from '../core/shell/types';
|
|
29
|
+
import type { ProcessDefinition, ProcessInstance, DesktopOSConfig } from '../core/desktop/types';
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
// Process Registry Store
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
interface ProcessRegistryState {
|
|
36
|
+
definitions: Map<string, ProcessDefinition>;
|
|
37
|
+
instances: Map<string, ProcessInstance>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let processIdCounter = 0;
|
|
41
|
+
function generateProcessInstanceId(): string {
|
|
42
|
+
return `process-${Date.now()}-${++processIdCounter}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// Context
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
interface DesktopOSContextValue {
|
|
50
|
+
// Window management
|
|
51
|
+
windows: UseWindowManagerReturn;
|
|
52
|
+
|
|
53
|
+
// Snap zones
|
|
54
|
+
snapZones: UseSnapZonesReturn;
|
|
55
|
+
|
|
56
|
+
// Taskbar
|
|
57
|
+
taskbar: UseTaskbarReturn;
|
|
58
|
+
|
|
59
|
+
// Process management
|
|
60
|
+
processes: {
|
|
61
|
+
definitions: ProcessDefinition[];
|
|
62
|
+
instances: ProcessInstance[];
|
|
63
|
+
launch: (processId: string, args?: Record<string, unknown>) => string | null;
|
|
64
|
+
terminate: (instanceId: string) => void;
|
|
65
|
+
getDefinition: (id: string) => ProcessDefinition | undefined;
|
|
66
|
+
getInstanceByWindow: (windowId: WindowId) => ProcessInstance | undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Configuration
|
|
70
|
+
config: {
|
|
71
|
+
enableSnapZones: boolean;
|
|
72
|
+
enableWindowGroups: boolean;
|
|
73
|
+
enableAnimations: boolean;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DesktopOSContext = createContext<DesktopOSContextValue | null>(null);
|
|
78
|
+
|
|
79
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
// Hook
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Access the desktop OS context.
|
|
85
|
+
*
|
|
86
|
+
* @throws Error if used outside DesktopOSProvider
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const { windows, processes, taskbar } = useDesktopOS();
|
|
91
|
+
*
|
|
92
|
+
* const handleLaunch = () => {
|
|
93
|
+
* processes.launch('my-app');
|
|
94
|
+
* };
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function useDesktopOS(): DesktopOSContextValue {
|
|
98
|
+
const context = useContext(DesktopOSContext);
|
|
99
|
+
if (!context) {
|
|
100
|
+
throw new Error('useDesktopOS must be used within a DesktopOSProvider');
|
|
101
|
+
}
|
|
102
|
+
return context;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if we're inside a DesktopOSProvider.
|
|
107
|
+
* Useful for components that work in both standalone and managed modes.
|
|
108
|
+
*/
|
|
109
|
+
export function useIsInDesktopOS(): boolean {
|
|
110
|
+
return useContext(DesktopOSContext) !== null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// Provider
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
export interface DesktopOSProviderProps extends DesktopOSConfig {
|
|
118
|
+
children: React.ReactNode;
|
|
119
|
+
/** Theme customization */
|
|
120
|
+
theme?: PartialDesktopOSTheme;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Main provider for desktop OS functionality.
|
|
125
|
+
*
|
|
126
|
+
* Wires together window management, process registry, taskbar, and theming.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```tsx
|
|
130
|
+
* const myApps: ProcessDefinition[] = [
|
|
131
|
+
* {
|
|
132
|
+
* id: 'notepad',
|
|
133
|
+
* name: 'Notepad',
|
|
134
|
+
* icon: <NotepadIcon />,
|
|
135
|
+
* component: NotepadApp,
|
|
136
|
+
* defaultSize: { width: 600, height: 400 },
|
|
137
|
+
* },
|
|
138
|
+
* ];
|
|
139
|
+
*
|
|
140
|
+
* function App() {
|
|
141
|
+
* return (
|
|
142
|
+
* <DesktopOSProvider
|
|
143
|
+
* processes={myApps}
|
|
144
|
+
* theme={{ colors: { accent: '#00ff88' } }}
|
|
145
|
+
* enableSnapZones
|
|
146
|
+
* enableWindowGroups
|
|
147
|
+
* >
|
|
148
|
+
* <Desktop>
|
|
149
|
+
* <WindowManager />
|
|
150
|
+
* </Desktop>
|
|
151
|
+
* <Taskbar />
|
|
152
|
+
* <ContextMenu />
|
|
153
|
+
* </DesktopOSProvider>
|
|
154
|
+
* );
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function DesktopOSProvider({
|
|
159
|
+
children,
|
|
160
|
+
processes: processDefinitions,
|
|
161
|
+
initialWindows,
|
|
162
|
+
initialPinnedApps,
|
|
163
|
+
theme,
|
|
164
|
+
onWindowsChange,
|
|
165
|
+
onPinnedAppsChange,
|
|
166
|
+
enableSnapZones = true,
|
|
167
|
+
enableWindowGroups = true,
|
|
168
|
+
enableAnimations = true,
|
|
169
|
+
}: DesktopOSProviderProps) {
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Process Registry State
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const [processState, setProcessState] = React.useState<ProcessRegistryState>(
|
|
175
|
+
() => ({
|
|
176
|
+
definitions: new Map(processDefinitions.map((d) => [d.id, d])),
|
|
177
|
+
instances: new Map(),
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Update definitions when prop changes
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
setProcessState((prev) => ({
|
|
184
|
+
...prev,
|
|
185
|
+
definitions: new Map(processDefinitions.map((d) => [d.id, d])),
|
|
186
|
+
}));
|
|
187
|
+
}, [processDefinitions]);
|
|
188
|
+
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// Core Hooks
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const windowManager = useWindowManager();
|
|
194
|
+
const snapZones = useSnapZones();
|
|
195
|
+
|
|
196
|
+
// Build window info for taskbar
|
|
197
|
+
const windowsForTaskbar = useMemo(
|
|
198
|
+
() =>
|
|
199
|
+
windowManager.windows.map((w) => {
|
|
200
|
+
// Find the process instance for this window
|
|
201
|
+
const instance = Array.from(processState.instances.values()).find(
|
|
202
|
+
(i) => i.windowId === w.id
|
|
203
|
+
);
|
|
204
|
+
return {
|
|
205
|
+
id: w.id,
|
|
206
|
+
appId: instance?.processId ?? 'unknown',
|
|
207
|
+
title: w.title,
|
|
208
|
+
icon: w.icon,
|
|
209
|
+
};
|
|
210
|
+
}),
|
|
211
|
+
[windowManager.windows, processState.instances]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const taskbar = useTaskbar(windowsForTaskbar, windowManager.focusedId);
|
|
215
|
+
|
|
216
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
217
|
+
// Process Management
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const launch = useCallback(
|
|
221
|
+
(processId: string, args?: Record<string, unknown>): string | null => {
|
|
222
|
+
const definition = processState.definitions.get(processId);
|
|
223
|
+
if (!definition) {
|
|
224
|
+
console.warn(`Process not found: ${processId}`);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check singleton constraint
|
|
229
|
+
if (definition.singleton) {
|
|
230
|
+
const existing = Array.from(processState.instances.values()).find(
|
|
231
|
+
(i) => i.processId === processId
|
|
232
|
+
);
|
|
233
|
+
if (existing) {
|
|
234
|
+
// Focus existing window instead
|
|
235
|
+
windowManager.focus(existing.windowId);
|
|
236
|
+
return existing.id;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Open window
|
|
241
|
+
const windowId = windowManager.open({
|
|
242
|
+
title: definition.name,
|
|
243
|
+
icon: typeof definition.icon === 'string' ? definition.icon : undefined,
|
|
244
|
+
size: definition.defaultSize,
|
|
245
|
+
minSize: definition.minSize,
|
|
246
|
+
maxSize: definition.maxSize,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Create process instance
|
|
250
|
+
const instanceId = generateProcessInstanceId();
|
|
251
|
+
const instance: ProcessInstance = {
|
|
252
|
+
id: instanceId,
|
|
253
|
+
processId,
|
|
254
|
+
windowId,
|
|
255
|
+
args,
|
|
256
|
+
startedAt: Date.now(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
setProcessState((prev) => {
|
|
260
|
+
const newInstances = new Map(prev.instances);
|
|
261
|
+
newInstances.set(instanceId, instance);
|
|
262
|
+
return { ...prev, instances: newInstances };
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return instanceId;
|
|
266
|
+
},
|
|
267
|
+
[processState.definitions, processState.instances, windowManager]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const terminate = useCallback(
|
|
271
|
+
(instanceId: string) => {
|
|
272
|
+
const instance = processState.instances.get(instanceId);
|
|
273
|
+
if (!instance) return;
|
|
274
|
+
|
|
275
|
+
// Close the window
|
|
276
|
+
windowManager.close(instance.windowId);
|
|
277
|
+
|
|
278
|
+
// Remove process instance
|
|
279
|
+
setProcessState((prev) => {
|
|
280
|
+
const newInstances = new Map(prev.instances);
|
|
281
|
+
newInstances.delete(instanceId);
|
|
282
|
+
return { ...prev, instances: newInstances };
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
[processState.instances, windowManager]
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const getDefinition = useCallback(
|
|
289
|
+
(id: string) => processState.definitions.get(id),
|
|
290
|
+
[processState.definitions]
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const getInstanceByWindow = useCallback(
|
|
294
|
+
(windowId: WindowId) =>
|
|
295
|
+
Array.from(processState.instances.values()).find(
|
|
296
|
+
(i) => i.windowId === windowId
|
|
297
|
+
),
|
|
298
|
+
[processState.instances]
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
302
|
+
// Callbacks
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
// Notify on windows change
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
onWindowsChange?.(windowManager.windows);
|
|
308
|
+
}, [windowManager.windows, onWindowsChange]);
|
|
309
|
+
|
|
310
|
+
// Clean up process instances when windows close
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const windowIds = new Set(windowManager.windows.map((w) => w.id));
|
|
313
|
+
|
|
314
|
+
setProcessState((prev) => {
|
|
315
|
+
let changed = false;
|
|
316
|
+
const newInstances = new Map(prev.instances);
|
|
317
|
+
|
|
318
|
+
for (const [instanceId, instance] of prev.instances) {
|
|
319
|
+
if (!windowIds.has(instance.windowId)) {
|
|
320
|
+
newInstances.delete(instanceId);
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return changed ? { ...prev, instances: newInstances } : prev;
|
|
326
|
+
});
|
|
327
|
+
}, [windowManager.windows]);
|
|
328
|
+
|
|
329
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
330
|
+
// Initialize pinned apps
|
|
331
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
const pinnedAppsInitialized = useRef(false);
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
// Only initialize once, even if initialPinnedApps changes
|
|
336
|
+
if (pinnedAppsInitialized.current) return;
|
|
337
|
+
if (initialPinnedApps && initialPinnedApps.length > 0) {
|
|
338
|
+
pinnedAppsInitialized.current = true;
|
|
339
|
+
const taskbarStore = useTaskbarStore.getState();
|
|
340
|
+
for (const appId of initialPinnedApps) {
|
|
341
|
+
taskbarStore.pinApp(appId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}, [initialPinnedApps]);
|
|
345
|
+
|
|
346
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
347
|
+
// Context Value
|
|
348
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
const contextValue = useMemo<DesktopOSContextValue>(
|
|
351
|
+
() => ({
|
|
352
|
+
windows: windowManager,
|
|
353
|
+
snapZones,
|
|
354
|
+
taskbar,
|
|
355
|
+
processes: {
|
|
356
|
+
definitions: Array.from(processState.definitions.values()),
|
|
357
|
+
instances: Array.from(processState.instances.values()),
|
|
358
|
+
launch,
|
|
359
|
+
terminate,
|
|
360
|
+
getDefinition,
|
|
361
|
+
getInstanceByWindow,
|
|
362
|
+
},
|
|
363
|
+
config: {
|
|
364
|
+
enableSnapZones,
|
|
365
|
+
enableWindowGroups,
|
|
366
|
+
enableAnimations,
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
[
|
|
370
|
+
windowManager,
|
|
371
|
+
snapZones,
|
|
372
|
+
taskbar,
|
|
373
|
+
processState,
|
|
374
|
+
launch,
|
|
375
|
+
terminate,
|
|
376
|
+
getDefinition,
|
|
377
|
+
getInstanceByWindow,
|
|
378
|
+
enableSnapZones,
|
|
379
|
+
enableWindowGroups,
|
|
380
|
+
enableAnimations,
|
|
381
|
+
]
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<ThemeProvider theme={theme}>
|
|
386
|
+
<DesktopOSContext.Provider value={contextValue}>
|
|
387
|
+
{children}
|
|
388
|
+
</DesktopOSContext.Provider>
|
|
389
|
+
</ThemeProvider>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @backbay/glia Desktop OS - ThemeProvider
|
|
3
|
+
*
|
|
4
|
+
* Injects theme CSS variables into the DOM.
|
|
5
|
+
* Can be used standalone or wrapped by DesktopOSProvider.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { createContext, useContext, useRef, useMemo, useLayoutEffect, useEffect } from 'react';
|
|
11
|
+
|
|
12
|
+
// SSR-safe useLayoutEffect - avoids React warnings during server rendering
|
|
13
|
+
const useIsomorphicLayoutEffect =
|
|
14
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
15
|
+
import {
|
|
16
|
+
type DesktopOSTheme,
|
|
17
|
+
type PartialDesktopOSTheme,
|
|
18
|
+
themeToCssVariables,
|
|
19
|
+
mergeTheme,
|
|
20
|
+
} from '../themes/types';
|
|
21
|
+
import { defaultTheme } from '../themes/default';
|
|
22
|
+
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// Context
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
interface ThemeContextValue {
|
|
28
|
+
theme: DesktopOSTheme;
|
|
29
|
+
cssVariables: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
33
|
+
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// Hook
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Access the current theme.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { theme } = useDesktopTheme();
|
|
44
|
+
* console.log(theme.colors.accent);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function useDesktopTheme(): ThemeContextValue {
|
|
48
|
+
const context = useContext(ThemeContext);
|
|
49
|
+
if (!context) {
|
|
50
|
+
// Return default theme if not in provider (standalone mode)
|
|
51
|
+
return {
|
|
52
|
+
theme: defaultTheme,
|
|
53
|
+
cssVariables: themeToCssVariables(defaultTheme),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return context;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
// Provider
|
|
61
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
export interface ThemeProviderProps {
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
/** Custom theme or partial theme to merge with default */
|
|
66
|
+
theme?: PartialDesktopOSTheme;
|
|
67
|
+
/** Target element to inject CSS variables (defaults to :root) */
|
|
68
|
+
target?: 'root' | 'local';
|
|
69
|
+
/** Class name for the wrapper div (only used when target='local') */
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Provides theme context and injects CSS variables.
|
|
75
|
+
*
|
|
76
|
+
* @example Global injection (recommended)
|
|
77
|
+
* ```tsx
|
|
78
|
+
* <ThemeProvider theme={{ colors: { accent: '#00ff88' } }}>
|
|
79
|
+
* <App />
|
|
80
|
+
* </ThemeProvider>
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Local injection (for isolated instances)
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <ThemeProvider target="local" className="my-desktop">
|
|
86
|
+
* <Desktop />
|
|
87
|
+
* </ThemeProvider>
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function ThemeProvider({
|
|
91
|
+
children,
|
|
92
|
+
theme: partialTheme,
|
|
93
|
+
target = 'root',
|
|
94
|
+
className,
|
|
95
|
+
}: ThemeProviderProps) {
|
|
96
|
+
const deprecationWarned = useRef(false);
|
|
97
|
+
if (!deprecationWarned.current) {
|
|
98
|
+
deprecationWarned.current = true;
|
|
99
|
+
console.warn("[Glia] DesktopOS ThemeProvider is deprecated. Use GliaThemeProvider instead.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Merge custom theme with default
|
|
103
|
+
const theme = useMemo(
|
|
104
|
+
() => (partialTheme ? mergeTheme(defaultTheme, partialTheme) : defaultTheme),
|
|
105
|
+
[partialTheme]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Convert to CSS variables
|
|
109
|
+
const cssVariables = useMemo(() => themeToCssVariables(theme), [theme]);
|
|
110
|
+
|
|
111
|
+
// Inject CSS variables into :root
|
|
112
|
+
useIsomorphicLayoutEffect(() => {
|
|
113
|
+
if (target !== 'root') return;
|
|
114
|
+
|
|
115
|
+
const root = document.documentElement;
|
|
116
|
+
const previousValues: Record<string, string> = {};
|
|
117
|
+
|
|
118
|
+
// Store previous values and set new ones
|
|
119
|
+
for (const [key, value] of Object.entries(cssVariables)) {
|
|
120
|
+
previousValues[key] = root.style.getPropertyValue(key);
|
|
121
|
+
root.style.setProperty(key, value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cleanup: restore previous values
|
|
125
|
+
return () => {
|
|
126
|
+
for (const [key, value] of Object.entries(previousValues)) {
|
|
127
|
+
if (value) {
|
|
128
|
+
root.style.setProperty(key, value);
|
|
129
|
+
} else {
|
|
130
|
+
root.style.removeProperty(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, [cssVariables, target]);
|
|
135
|
+
|
|
136
|
+
const contextValue = useMemo(
|
|
137
|
+
() => ({ theme, cssVariables }),
|
|
138
|
+
[theme, cssVariables]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Local injection wraps children in a div with inline CSS variables
|
|
142
|
+
if (target === 'local') {
|
|
143
|
+
const style: React.CSSProperties = {};
|
|
144
|
+
for (const [key, value] of Object.entries(cssVariables)) {
|
|
145
|
+
(style as Record<string, string>)[key] = value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<ThemeContext.Provider value={contextValue}>
|
|
150
|
+
<div className={className} style={style}>
|
|
151
|
+
{children}
|
|
152
|
+
</div>
|
|
153
|
+
</ThemeContext.Provider>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ThemeContext.Provider value={contextValue}>
|
|
159
|
+
{children}
|
|
160
|
+
</ThemeContext.Provider>
|
|
161
|
+
);
|
|
162
|
+
}
|