@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
|
@@ -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
|
+
}
|