@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,230 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - Snap Zone Overlay Component
3
+ *
4
+ * Visual feedback overlay showing the target snap zone during window dragging.
5
+ * Uses CSS variables for theming (--glia-color-*, etc.)
6
+ */
7
+
8
+ import React, { useMemo } from 'react';
9
+ import { useSnapZones, getSnapZoneDimensions } from '../../core/desktop/useSnapZones';
10
+ import type { SnapZone } from '../../core/desktop/types';
11
+
12
+ // ═══════════════════════════════════════════════════════════════════════════
13
+ // Types
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+
16
+ export interface SnapZoneOverlayProps {
17
+ /** Override the active zone (uses hook state by default) */
18
+ zone?: SnapZone | null;
19
+ /** Custom color for the overlay */
20
+ color?: string;
21
+ /** Additional CSS class */
22
+ className?: string;
23
+ /** Inline styles */
24
+ style?: React.CSSProperties;
25
+ }
26
+
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // Styles (using CSS custom properties)
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ const containerStyles: React.CSSProperties = {
32
+ position: 'fixed',
33
+ inset: 0,
34
+ pointerEvents: 'none',
35
+ zIndex: 9998, // Just below windows being dragged
36
+ };
37
+
38
+ const createPreviewStyles = (
39
+ dimensions: { x: number; y: number; width: number; height: number },
40
+ color?: string
41
+ ): React.CSSProperties => ({
42
+ position: 'absolute',
43
+ left: dimensions.x,
44
+ top: dimensions.y,
45
+ width: dimensions.width,
46
+ height: dimensions.height,
47
+ background: color ?? 'var(--glia-glass-active-shadow, rgba(212, 168, 75, 0.15))',
48
+ border: `2px solid ${color ?? 'var(--glia-color-accent, #d4a84b)'}`,
49
+ borderRadius: 'var(--glia-radius-lg, 3px)',
50
+ transition: 'all 150ms var(--glia-easing-spring, cubic-bezier(0.34, 1.56, 0.64, 1))',
51
+ // Add subtle inner glow
52
+ boxShadow: `
53
+ inset 0 0 60px 10px ${color ?? 'var(--glia-glass-active-shadow, rgba(212, 168, 75, 0.1))'},
54
+ 0 0 40px 5px ${color ?? 'var(--glia-glass-active-shadow, rgba(212, 168, 75, 0.1))'}
55
+ `,
56
+ });
57
+
58
+ // Zone label styles (shows which zone is being targeted)
59
+ const labelStyles: React.CSSProperties = {
60
+ position: 'absolute',
61
+ top: '50%',
62
+ left: '50%',
63
+ transform: 'translate(-50%, -50%)',
64
+ fontFamily: 'var(--glia-font-mono, "JetBrains Mono", monospace)',
65
+ fontSize: '12px',
66
+ fontWeight: 600,
67
+ letterSpacing: '0.1em',
68
+ textTransform: 'uppercase',
69
+ color: 'var(--glia-color-accent, #d4a84b)',
70
+ opacity: 0.6,
71
+ userSelect: 'none',
72
+ whiteSpace: 'nowrap',
73
+ };
74
+
75
+ // ═══════════════════════════════════════════════════════════════════════════
76
+ // Helper Functions
77
+ // ═══════════════════════════════════════════════════════════════════════════
78
+
79
+ /**
80
+ * Get human-readable label for snap zone.
81
+ */
82
+ function getZoneLabel(zone: SnapZone): string {
83
+ switch (zone) {
84
+ case 'maximize':
85
+ return 'Maximize';
86
+ case 'left':
87
+ return 'Left Half';
88
+ case 'right':
89
+ return 'Right Half';
90
+ case 'top-left':
91
+ return 'Top Left';
92
+ case 'top-right':
93
+ return 'Top Right';
94
+ case 'bottom-left':
95
+ return 'Bottom Left';
96
+ case 'bottom-right':
97
+ return 'Bottom Right';
98
+ }
99
+ }
100
+
101
+ // ═══════════════════════════════════════════════════════════════════════════
102
+ // Component
103
+ // ═══════════════════════════════════════════════════════════════════════════
104
+
105
+ /**
106
+ * Snap zone overlay component.
107
+ *
108
+ * Displays a semi-transparent preview rectangle showing where a window
109
+ * will snap to when released during a drag operation.
110
+ *
111
+ * @example Using with hook state (automatic)
112
+ * ```tsx
113
+ * const { activeZone } = useSnapZones();
114
+ *
115
+ * return (
116
+ * <>
117
+ * {activeZone && <SnapZoneOverlay />}
118
+ * </>
119
+ * );
120
+ * ```
121
+ *
122
+ * @example Manual control
123
+ * ```tsx
124
+ * <SnapZoneOverlay
125
+ * zone="left"
126
+ * color="rgba(0, 255, 128, 0.2)"
127
+ * />
128
+ * ```
129
+ */
130
+ export function SnapZoneOverlay({
131
+ zone: zoneProp,
132
+ color,
133
+ className,
134
+ style,
135
+ }: SnapZoneOverlayProps) {
136
+ const { activeZone: hookZone } = useSnapZones();
137
+ const zone = zoneProp ?? hookZone;
138
+
139
+ // Calculate preview dimensions
140
+ const dimensions = useMemo(() => {
141
+ if (!zone) return null;
142
+ return getSnapZoneDimensions(zone);
143
+ }, [zone]);
144
+
145
+ // Create preview styles
146
+ const previewStyles = useMemo(() => {
147
+ if (!dimensions) return null;
148
+ return createPreviewStyles(dimensions, color);
149
+ }, [dimensions, color]);
150
+
151
+ // Don't render if no active zone
152
+ if (!zone || !dimensions || !previewStyles) {
153
+ return null;
154
+ }
155
+
156
+ return (
157
+ <div
158
+ className={className}
159
+ style={{ ...containerStyles, ...style }}
160
+ data-bb-snap-zone-overlay
161
+ data-zone={zone}
162
+ >
163
+ {/* Preview rectangle */}
164
+ <div style={previewStyles} data-bb-snap-preview>
165
+ {/* Zone label */}
166
+ <span style={labelStyles} data-bb-snap-label>
167
+ {getZoneLabel(zone)}
168
+ </span>
169
+ </div>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ // ═══════════════════════════════════════════════════════════════════════════
175
+ // Standalone Preview (for use without hooks)
176
+ // ═══════════════════════════════════════════════════════════════════════════
177
+
178
+ export interface SnapZonePreviewProps {
179
+ /** The snap zone to preview */
180
+ zone: SnapZone;
181
+ /** Custom color for the overlay */
182
+ color?: string;
183
+ /** Whether to show the label */
184
+ showLabel?: boolean;
185
+ /** Additional CSS class */
186
+ className?: string;
187
+ /** Inline styles */
188
+ style?: React.CSSProperties;
189
+ }
190
+
191
+ /**
192
+ * Standalone snap zone preview without hook dependency.
193
+ *
194
+ * Useful for testing or custom implementations.
195
+ *
196
+ * @example
197
+ * ```tsx
198
+ * <SnapZonePreview zone="left" showLabel={false} />
199
+ * ```
200
+ */
201
+ export function SnapZonePreview({
202
+ zone,
203
+ color,
204
+ showLabel = true,
205
+ className,
206
+ style,
207
+ }: SnapZonePreviewProps) {
208
+ const dimensions = useMemo(() => getSnapZoneDimensions(zone), [zone]);
209
+ const previewStyles = useMemo(
210
+ () => createPreviewStyles(dimensions, color),
211
+ [dimensions, color]
212
+ );
213
+
214
+ return (
215
+ <div
216
+ className={className}
217
+ style={{ ...containerStyles, ...style }}
218
+ data-bb-snap-zone-preview
219
+ data-zone={zone}
220
+ >
221
+ <div style={previewStyles} data-bb-snap-preview>
222
+ {showLabel && (
223
+ <span style={labelStyles} data-bb-snap-label>
224
+ {getZoneLabel(zone)}
225
+ </span>
226
+ )}
227
+ </div>
228
+ </div>
229
+ );
230
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - Desktop Components
3
+ *
4
+ * Components for the main desktop surface, icons, and snap zones.
5
+ */
6
+
7
+ export { Desktop, type DesktopProps } from './Desktop';
8
+ export { DesktopIcon, type DesktopIconProps } from './DesktopIcon';
9
+ export {
10
+ SnapZoneOverlay,
11
+ SnapZonePreview,
12
+ type SnapZoneOverlayProps,
13
+ type SnapZonePreviewProps,
14
+ } from './SnapZoneOverlay';
15
+ export { FileBrowser } from './FileBrowser';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - Components
3
+ *
4
+ * Styled UI components for the desktop OS layer.
5
+ * All components use CSS variables for theming and can work
6
+ * standalone or within the DesktopOSProvider context.
7
+ */
8
+
9
+ // Desktop components (desktop surface, icons, snap zones)
10
+ export * from './desktop';
11
+
12
+ // Shell components (taskbar, context menu, clock)
13
+ export * from './shell';
14
+
15
+ // Window components (window container, titlebar)
16
+ export * from './window';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @backbay/glia Desktop OS - Clock Component
3
+ *
4
+ * System clock for the taskbar. Displays time in Roman numerals (Backbay style)
5
+ * with a tooltip showing the full date on hover.
6
+ *
7
+ * Can be used standalone or within the DesktopOSProvider context.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // Basic usage
12
+ * <Clock />
13
+ *
14
+ * // With custom format
15
+ * <Clock format="standard" />
16
+ *
17
+ * // Styled with CSS variables
18
+ * <Clock style={{ '--glia-font-mono': 'Monaco, monospace' }} />
19
+ * ```
20
+ */
21
+
22
+ import { useState, useEffect, type CSSProperties } from 'react';
23
+
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+ // Types
26
+ // ═══════════════════════════════════════════════════════════════════════════
27
+
28
+ export interface ClockProps {
29
+ /** Time display format */
30
+ format?: 'roman' | 'standard' | '24h';
31
+ /** Whether to show seconds */
32
+ showSeconds?: boolean;
33
+ /** Whether to show date tooltip on hover */
34
+ showTooltip?: boolean;
35
+ /** Additional CSS class name */
36
+ className?: string;
37
+ /** Additional inline styles */
38
+ style?: CSSProperties;
39
+ }
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // Roman Numeral Conversion
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ const ROMAN_NUMERALS: [number, string][] = [
46
+ [1000, 'M'],
47
+ [900, 'CM'],
48
+ [500, 'D'],
49
+ [400, 'CD'],
50
+ [100, 'C'],
51
+ [90, 'XC'],
52
+ [50, 'L'],
53
+ [40, 'XL'],
54
+ [10, 'X'],
55
+ [9, 'IX'],
56
+ [5, 'V'],
57
+ [4, 'IV'],
58
+ [1, 'I'],
59
+ ];
60
+
61
+ function toRomanNumeral(num: number): string {
62
+ if (num <= 0) return 'N'; // nulla (0)
63
+ let result = '';
64
+ let remaining = num;
65
+ for (const [value, symbol] of ROMAN_NUMERALS) {
66
+ while (remaining >= value) {
67
+ result += symbol;
68
+ remaining -= value;
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+
74
+ // ═══════════════════════════════════════════════════════════════════════════
75
+ // Styles
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+
78
+ const styles: Record<string, CSSProperties> = {
79
+ wrapper: {
80
+ position: 'relative',
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ height: '100%',
84
+ },
85
+ clock: {
86
+ fontFamily: 'var(--glia-font-mono)',
87
+ fontSize: '13px',
88
+ color: 'var(--glia-color-text-soft)',
89
+ padding: '0 12px',
90
+ letterSpacing: '0.05em',
91
+ cursor: 'default',
92
+ userSelect: 'none',
93
+ transition: 'color var(--glia-duration-fast, 100ms) ease',
94
+ },
95
+ clockHover: {
96
+ color: 'var(--glia-color-text-muted)',
97
+ },
98
+ tooltip: {
99
+ position: 'absolute',
100
+ bottom: '100%',
101
+ right: 0,
102
+ marginBottom: '8px',
103
+ padding: '8px 12px',
104
+ background: 'var(--glia-color-bg-elevated, #111111)',
105
+ border: '1px solid var(--glia-color-border, #333333)',
106
+ borderRadius: 'var(--glia-radius-md, 3px)',
107
+ fontFamily: 'var(--glia-font-body)',
108
+ fontSize: '12px',
109
+ color: 'var(--glia-color-text-muted)',
110
+ whiteSpace: 'nowrap',
111
+ boxShadow: 'var(--glia-shadow-soft)',
112
+ zIndex: 10000,
113
+ },
114
+ };
115
+
116
+ // ═══════════════════════════════════════════════════════════════════════════
117
+ // Component
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+
120
+ export function Clock({
121
+ format = 'roman',
122
+ showSeconds = false,
123
+ showTooltip: enableTooltip = true,
124
+ className,
125
+ style,
126
+ }: ClockProps) {
127
+ const [time, setTime] = useState<Date | null>(null);
128
+ const [showTooltip, setShowTooltip] = useState(false);
129
+ const [isHovered, setIsHovered] = useState(false);
130
+
131
+ // Initialize on client side only (SSR-safe)
132
+ useEffect(() => {
133
+ setTime(new Date());
134
+
135
+ const interval = setInterval(() => {
136
+ setTime(new Date());
137
+ }, showSeconds ? 1000 : 60000);
138
+
139
+ return () => clearInterval(interval);
140
+ }, [showSeconds]);
141
+
142
+ // Don't render until client-side hydrated
143
+ if (!time) return null;
144
+
145
+ const hours = time.getHours();
146
+ const minutes = time.getMinutes();
147
+ const seconds = time.getSeconds();
148
+
149
+ let formattedTime: string;
150
+
151
+ switch (format) {
152
+ case 'roman': {
153
+ const romanHours = toRomanNumeral(hours === 0 ? 24 : hours);
154
+ const romanMinutes = toRomanNumeral(minutes);
155
+ formattedTime = showSeconds
156
+ ? `${romanHours}:${romanMinutes}:${toRomanNumeral(seconds)}`
157
+ : `${romanHours}:${romanMinutes || 'N'}`;
158
+ break;
159
+ }
160
+ case '24h': {
161
+ const pad = (n: number) => n.toString().padStart(2, '0');
162
+ formattedTime = showSeconds
163
+ ? `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
164
+ : `${pad(hours)}:${pad(minutes)}`;
165
+ break;
166
+ }
167
+ case 'standard':
168
+ default: {
169
+ const h = hours % 12 || 12;
170
+ const ampm = hours >= 12 ? 'PM' : 'AM';
171
+ const pad = (n: number) => n.toString().padStart(2, '0');
172
+ formattedTime = showSeconds
173
+ ? `${h}:${pad(minutes)}:${pad(seconds)} ${ampm}`
174
+ : `${h}:${pad(minutes)} ${ampm}`;
175
+ break;
176
+ }
177
+ }
178
+
179
+ const fullDate = time.toLocaleDateString('en-US', {
180
+ weekday: 'long',
181
+ year: 'numeric',
182
+ month: 'long',
183
+ day: 'numeric',
184
+ });
185
+
186
+ return (
187
+ <div
188
+ style={{ ...styles.wrapper, ...style }}
189
+ className={className}
190
+ onMouseEnter={() => {
191
+ setShowTooltip(true);
192
+ setIsHovered(true);
193
+ }}
194
+ onMouseLeave={() => {
195
+ setShowTooltip(false);
196
+ setIsHovered(false);
197
+ }}
198
+ >
199
+ <div
200
+ style={{
201
+ ...styles.clock,
202
+ ...(isHovered ? styles.clockHover : {}),
203
+ }}
204
+ >
205
+ {formattedTime}
206
+ </div>
207
+ {enableTooltip && showTooltip && (
208
+ <div style={styles.tooltip}>{fullDate}</div>
209
+ )}
210
+ </div>
211
+ );
212
+ }