@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
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';
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -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
+ }