@1889ca/ui 0.5.0 → 0.6.0

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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/components/ContextMenu.css +5 -3
  3. package/src/components/ContextMenu.jsx +2 -14
  4. package/src/components/DraggablePanel.css +2 -1
  5. package/src/components/DraggablePanel.jsx +4 -17
  6. package/src/components/EmptyState.css +36 -0
  7. package/src/components/EmptyState.jsx +27 -0
  8. package/src/components/ItemCard.css +12 -1
  9. package/src/components/ItemCard.jsx +8 -3
  10. package/src/components/ItemGrid.css +22 -0
  11. package/src/components/Lightbox.css +12 -2
  12. package/src/components/Lightbox.jsx +2 -2
  13. package/src/components/LoginCard.css +1 -1
  14. package/src/components/Modal.css +5 -4
  15. package/src/components/PageHeader.css +15 -1
  16. package/src/components/Panel.css +53 -2
  17. package/src/components/Panel.jsx +25 -5
  18. package/src/components/Skeleton.css +79 -0
  19. package/src/components/Skeleton.jsx +47 -0
  20. package/src/components/SlidePanel.css +7 -1
  21. package/src/components/StatusBar.css +38 -1
  22. package/src/components/TabBar.css +53 -0
  23. package/src/components/TabBar.jsx +28 -0
  24. package/src/components/ThemeToggle.css +34 -0
  25. package/src/components/ThemeToggle.jsx +44 -0
  26. package/src/components/Toast.css +102 -0
  27. package/src/components/Toast.jsx +67 -0
  28. package/src/components/Toggle.css +53 -0
  29. package/src/components/Toggle.jsx +28 -0
  30. package/src/components/Tooltip.css +36 -0
  31. package/src/components/Tooltip.jsx +55 -0
  32. package/src/components/UserMenu.css +18 -1
  33. package/src/components/UserMenu.jsx +28 -18
  34. package/src/effects/Particles.jsx +117 -0
  35. package/src/effects/ambient-glow.css +67 -0
  36. package/src/effects/edge-light.css +69 -0
  37. package/src/effects/glass-tint.css +47 -0
  38. package/src/effects/glow-focus.css +64 -0
  39. package/src/effects/haptic.css +52 -0
  40. package/src/effects/particles.css +10 -0
  41. package/src/effects/scroll-parallax.css +46 -0
  42. package/src/hooks/useClickOutside.js +26 -0
  43. package/src/hooks/useLocalStorage.js +25 -0
  44. package/src/hooks/useMagnet.js +60 -0
  45. package/src/hooks/useMediaQuery.js +24 -0
  46. package/src/hooks/useScrollParallax.js +39 -0
  47. package/src/hooks/useSound.js +106 -0
  48. package/src/hooks/useTheme.js +90 -0
  49. package/src/index.js +36 -0
  50. package/src/styles/primitives.css +48 -1
  51. package/src/styles/tokens.css +89 -0
  52. package/src/utils/accentColors.js +85 -0
  53. package/src/utils/viewTransition.js +16 -0
@@ -4,7 +4,7 @@
4
4
  position: fixed;
5
5
  top: 0;
6
6
  height: 100dvh;
7
- background: rgba(12, 12, 18, 0.85);
7
+ background: var(--ui-glass-heavy);
8
8
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
9
9
  border-right: 1px solid var(--ui-border);
10
10
  transition: transform 0.3s var(--ui-ease);
@@ -58,6 +58,12 @@
58
58
  padding: 0.75rem;
59
59
  }
60
60
 
61
+ @media (max-width: 480px) {
62
+ .ui-slide-panel {
63
+ max-width: 85vw;
64
+ }
65
+ }
66
+
61
67
  /* Tab button primitive */
62
68
  .ui-tab {
63
69
  flex: 1;
@@ -7,7 +7,7 @@
7
7
  padding: 0 1rem;
8
8
  height: 28px;
9
9
  border-top: 1px solid var(--ui-border);
10
- background: rgba(8, 8, 12, 0.75);
10
+ background: var(--ui-glass-bar);
11
11
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
12
12
  box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.03);
13
13
  position: fixed;
@@ -18,6 +18,18 @@
18
18
  font-size: 0.72rem;
19
19
  color: var(--ui-text-muted);
20
20
  gap: 0.5rem;
21
+ overflow: hidden;
22
+ }
23
+
24
+ @media (max-width: 480px) {
25
+ .ui-statusbar {
26
+ padding: 0 0.5rem;
27
+ gap: 0.25rem;
28
+ }
29
+
30
+ .ui-statusbar-center {
31
+ display: none;
32
+ }
21
33
  }
22
34
 
23
35
  .ui-statusbar-left,
@@ -27,6 +39,7 @@
27
39
  align-items: center;
28
40
  gap: 0.6rem;
29
41
  min-width: 0;
42
+ overflow: hidden;
30
43
  }
31
44
 
32
45
  .ui-statusbar-left { justify-content: flex-start; }
@@ -116,3 +129,27 @@
116
129
  background: var(--ui-border);
117
130
  flex-shrink: 0;
118
131
  }
132
+
133
+ /* Progress shimmer */
134
+ .ui-sb-progress-fill {
135
+ position: relative;
136
+ overflow: hidden;
137
+ }
138
+
139
+ .ui-sb-progress-fill::after {
140
+ content: '';
141
+ position: absolute;
142
+ inset: 0;
143
+ background: linear-gradient(
144
+ 90deg,
145
+ transparent 0%,
146
+ rgba(255, 255, 255, 0.15) 50%,
147
+ transparent 100%
148
+ );
149
+ animation: ui-progress-shimmer 1.5s var(--ui-ease) infinite;
150
+ }
151
+
152
+ @keyframes ui-progress-shimmer {
153
+ from { transform: translateX(-100%); }
154
+ to { transform: translateX(100%); }
155
+ }
@@ -0,0 +1,53 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+
3
+ .ui-tabbar {
4
+ display: flex;
5
+ gap: 2px;
6
+ padding: 0 0.35rem;
7
+ border-bottom: 1px solid var(--ui-border);
8
+ flex-shrink: 0;
9
+ }
10
+
11
+ .ui-tabbar--full .ui-tabbar-tab {
12
+ flex: 1;
13
+ text-align: center;
14
+ }
15
+
16
+ .ui-tabbar-tab {
17
+ position: relative;
18
+ padding: 0.5rem 0.75rem;
19
+ background: none;
20
+ border: none;
21
+ color: var(--ui-text-subtle);
22
+ font-size: 0.72rem;
23
+ font-weight: 500;
24
+ text-transform: uppercase;
25
+ letter-spacing: 0.05em;
26
+ cursor: pointer;
27
+ transition: color 0.15s var(--ui-ease);
28
+ }
29
+
30
+ .ui-tabbar-tab::after {
31
+ content: '';
32
+ position: absolute;
33
+ left: 0.35rem;
34
+ right: 0.35rem;
35
+ bottom: -1px;
36
+ height: 2px;
37
+ border-radius: 1px;
38
+ background: transparent;
39
+ transition: background 0.2s var(--ui-ease), box-shadow 0.2s var(--ui-ease);
40
+ }
41
+
42
+ .ui-tabbar-tab:hover {
43
+ color: var(--ui-text-muted);
44
+ }
45
+
46
+ .ui-tabbar-tab--active {
47
+ color: var(--ui-accent);
48
+ }
49
+
50
+ .ui-tabbar-tab--active::after {
51
+ background: var(--ui-accent);
52
+ box-shadow: 0 0 8px var(--ui-accent-glow);
53
+ }
@@ -0,0 +1,28 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+ import './TabBar.css';
3
+
4
+ /**
5
+ * Standalone tab bar with animated underline indicator.
6
+ * @param {Array} tabs - [{ id, label }]
7
+ * @param {string} activeTab - Currently active tab id
8
+ * @param {function} onChange - Called with tab id
9
+ * @param {string} variant - 'default' | 'full-width' (default: 'default')
10
+ */
11
+ export default function TabBar({ tabs, activeTab, onChange, variant = 'default' }) {
12
+ return (
13
+ <nav className={`ui-tabbar${variant === 'full-width' ? ' ui-tabbar--full' : ''}`} role="tablist">
14
+ {tabs.map((tab) => (
15
+ <button
16
+ key={tab.id}
17
+ type="button"
18
+ role="tab"
19
+ aria-selected={activeTab === tab.id}
20
+ className={`ui-tabbar-tab${activeTab === tab.id ? ' ui-tabbar-tab--active' : ''}`}
21
+ onClick={() => onChange?.(tab.id)}
22
+ >
23
+ {tab.label}
24
+ </button>
25
+ ))}
26
+ </nav>
27
+ );
28
+ }
@@ -0,0 +1,34 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+
3
+ .ui-theme-toggle {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 32px;
8
+ height: 32px;
9
+ border-radius: var(--ui-radius-sm);
10
+ border: 1px solid var(--ui-border);
11
+ background: var(--ui-surface);
12
+ color: var(--ui-text-muted);
13
+ cursor: pointer;
14
+ padding: 0;
15
+ transition: color 0.2s var(--ui-ease), border-color 0.2s var(--ui-ease), background 0.2s var(--ui-ease), transform 0.2s var(--ui-ease-spring);
16
+ }
17
+
18
+ .ui-theme-toggle:hover {
19
+ color: var(--ui-text);
20
+ border-color: var(--ui-border-strong);
21
+ background: var(--ui-surface-raised);
22
+ }
23
+
24
+ .ui-theme-toggle:active {
25
+ transform: scale(0.92);
26
+ }
27
+
28
+ .ui-theme-toggle svg {
29
+ transition: transform 0.3s var(--ui-ease-spring);
30
+ }
31
+
32
+ .ui-theme-toggle:hover svg {
33
+ transform: rotate(15deg);
34
+ }
@@ -0,0 +1,44 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+ import useTheme from '../hooks/useTheme.js';
3
+
4
+ /**
5
+ * Theme toggle button — cycles between dark/light.
6
+ * Uses useTheme internally; can also accept external theme state.
7
+ *
8
+ * @param {string} [size='20'] - Icon size
9
+ * @param {string} [className] - Additional class names
10
+ * @param {object} [themeState] - External { resolvedTheme, toggle } from useTheme (optional)
11
+ */
12
+ export default function ThemeToggle({ size = 20, className, themeState }) {
13
+ const internal = useTheme();
14
+ const { resolvedTheme, toggle } = themeState || internal;
15
+ const isDark = resolvedTheme === 'dark';
16
+
17
+ return (
18
+ <button
19
+ type="button"
20
+ className={`ui-theme-toggle${className ? ` ${className}` : ''}`}
21
+ onClick={toggle}
22
+ aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
23
+ title={`Switch to ${isDark ? 'light' : 'dark'} mode`}
24
+ >
25
+ {isDark ? (
26
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
27
+ <circle cx="12" cy="12" r="5" />
28
+ <line x1="12" y1="1" x2="12" y2="3" />
29
+ <line x1="12" y1="21" x2="12" y2="23" />
30
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
31
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
32
+ <line x1="1" y1="12" x2="3" y2="12" />
33
+ <line x1="21" y1="12" x2="23" y2="12" />
34
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
35
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
36
+ </svg>
37
+ ) : (
38
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
39
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
40
+ </svg>
41
+ )}
42
+ </button>
43
+ );
44
+ }
@@ -0,0 +1,102 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+
3
+ .ui-toast-container {
4
+ position: fixed;
5
+ bottom: 40px;
6
+ right: 1rem;
7
+ z-index: 1100;
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 0.5rem;
11
+ pointer-events: none;
12
+ max-width: calc(100vw - 2rem);
13
+ }
14
+
15
+ .ui-toast {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 0.6rem;
19
+ padding: 0.6rem 1rem;
20
+ min-width: 220px;
21
+ max-width: 400px;
22
+ border-radius: var(--ui-radius);
23
+ border: 1px solid var(--ui-border);
24
+ background: var(--ui-glass-heavy);
25
+ backdrop-filter: blur(var(--ui-glass-blur-heavy));
26
+ box-shadow: var(--ui-shadow-float);
27
+ font-size: 0.82rem;
28
+ color: var(--ui-text);
29
+ cursor: pointer;
30
+ pointer-events: auto;
31
+ animation: ui-toast-in 0.35s var(--ui-ease-spring);
32
+ }
33
+
34
+ .ui-toast--leaving {
35
+ animation: ui-toast-out 0.3s var(--ui-ease) forwards;
36
+ }
37
+
38
+ @keyframes ui-toast-in {
39
+ from {
40
+ opacity: 0;
41
+ transform: translateX(100%) scale(0.95);
42
+ }
43
+ to {
44
+ opacity: 1;
45
+ transform: translateX(0) scale(1);
46
+ }
47
+ }
48
+
49
+ @keyframes ui-toast-out {
50
+ to {
51
+ opacity: 0;
52
+ transform: translateX(30%) scale(0.95);
53
+ max-height: 0;
54
+ padding-top: 0;
55
+ padding-bottom: 0;
56
+ margin-bottom: -0.5rem;
57
+ overflow: hidden;
58
+ }
59
+ }
60
+
61
+ .ui-toast-icon {
62
+ flex-shrink: 0;
63
+ width: 18px;
64
+ height: 18px;
65
+ border-radius: 50%;
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ font-size: 0.7rem;
70
+ font-weight: 700;
71
+ }
72
+
73
+ .ui-toast--info {
74
+ border-left: 3px solid var(--ui-accent);
75
+ }
76
+
77
+ .ui-toast--info .ui-toast-icon {
78
+ background: var(--ui-accent-muted);
79
+ color: var(--ui-accent);
80
+ }
81
+
82
+ .ui-toast--success {
83
+ border-left: 3px solid var(--ui-success);
84
+ }
85
+
86
+ .ui-toast--success .ui-toast-icon {
87
+ background: rgba(52, 211, 153, 0.15);
88
+ color: var(--ui-success);
89
+ }
90
+
91
+ .ui-toast--error {
92
+ border-left: 3px solid var(--ui-danger);
93
+ }
94
+
95
+ .ui-toast--error .ui-toast-icon {
96
+ background: var(--ui-danger-glow);
97
+ color: var(--ui-danger);
98
+ }
99
+
100
+ .ui-toast-message {
101
+ line-height: 1.4;
102
+ }
@@ -0,0 +1,67 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+ import { createContext, useContext, useState, useCallback, useRef } from 'react';
3
+ import './Toast.css';
4
+
5
+ const ToastContext = createContext(null);
6
+
7
+ let toastId = 0;
8
+
9
+ /**
10
+ * Wrap your app in ToastProvider to enable toasts.
11
+ * Use the useToast() hook to show toasts imperatively.
12
+ */
13
+ export function ToastProvider({ children }) {
14
+ const [toasts, setToasts] = useState([]);
15
+ const timers = useRef({});
16
+
17
+ const dismiss = useCallback((id) => {
18
+ setToasts(prev => prev.map(t => t.id === id ? { ...t, leaving: true } : t));
19
+ setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 300);
20
+ }, []);
21
+
22
+ const toast = useCallback(({ message, type = 'info', duration = 4000 }) => {
23
+ const id = ++toastId;
24
+ setToasts(prev => [...prev, { id, message, type, leaving: false }]);
25
+ if (duration > 0) {
26
+ timers.current[id] = setTimeout(() => dismiss(id), duration);
27
+ }
28
+ return id;
29
+ }, [dismiss]);
30
+
31
+ const api = useCallback((message, type) => {
32
+ if (typeof message === 'object') return toast(message);
33
+ return toast({ message, type });
34
+ }, [toast]);
35
+
36
+ api.success = (message, duration) => toast({ message, type: 'success', duration });
37
+ api.error = (message, duration) => toast({ message, type: 'error', duration: duration ?? 6000 });
38
+ api.info = (message, duration) => toast({ message, type: 'info', duration });
39
+
40
+ return (
41
+ <ToastContext.Provider value={api}>
42
+ {children}
43
+ <div className="ui-toast-container" aria-live="polite">
44
+ {toasts.map((t) => (
45
+ <div
46
+ key={t.id}
47
+ className={`ui-toast ui-toast--${t.type}${t.leaving ? ' ui-toast--leaving' : ''}`}
48
+ onClick={() => { clearTimeout(timers.current[t.id]); dismiss(t.id); }}
49
+ >
50
+ <span className="ui-toast-icon">{t.type === 'success' ? '✓' : t.type === 'error' ? '✕' : 'ℹ'}</span>
51
+ <span className="ui-toast-message">{t.message}</span>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ </ToastContext.Provider>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Returns the toast function.
61
+ * toast('message') or toast.success('done') / toast.error('failed') / toast.info('note')
62
+ */
63
+ export function useToast() {
64
+ const ctx = useContext(ToastContext);
65
+ if (!ctx) throw new Error('useToast requires <ToastProvider>');
66
+ return ctx;
67
+ }
@@ -0,0 +1,53 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+
3
+ .ui-toggle {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: 0.5rem;
7
+ cursor: pointer;
8
+ }
9
+
10
+ .ui-toggle--disabled {
11
+ opacity: 0.4;
12
+ cursor: not-allowed;
13
+ }
14
+
15
+ .ui-toggle-track {
16
+ position: relative;
17
+ width: 36px;
18
+ height: 20px;
19
+ border-radius: 10px;
20
+ border: 1px solid var(--ui-border-strong);
21
+ background: var(--ui-surface);
22
+ cursor: inherit;
23
+ padding: 0;
24
+ transition: background 0.2s var(--ui-ease), border-color 0.2s var(--ui-ease), box-shadow 0.2s var(--ui-ease);
25
+ }
26
+
27
+ .ui-toggle-track--on {
28
+ background: var(--ui-accent);
29
+ border-color: var(--ui-accent);
30
+ box-shadow: 0 0 12px var(--ui-accent-glow);
31
+ }
32
+
33
+ .ui-toggle-knob {
34
+ position: absolute;
35
+ top: 2px;
36
+ left: 2px;
37
+ width: 14px;
38
+ height: 14px;
39
+ border-radius: 50%;
40
+ background: var(--ui-text-muted);
41
+ transition: transform 0.2s var(--ui-ease-spring), background 0.2s var(--ui-ease);
42
+ }
43
+
44
+ .ui-toggle-track--on .ui-toggle-knob {
45
+ transform: translateX(16px);
46
+ background: #fff;
47
+ }
48
+
49
+ .ui-toggle-label {
50
+ font-size: 0.82rem;
51
+ color: var(--ui-text-muted);
52
+ user-select: none;
53
+ }
@@ -0,0 +1,28 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+ import './Toggle.css';
3
+
4
+ /**
5
+ * Toggle switch with animated knob.
6
+ * @param {boolean} checked
7
+ * @param {function} onChange - called with new boolean value
8
+ * @param {string} label - Optional accessible label
9
+ * @param {boolean} disabled
10
+ */
11
+ export default function Toggle({ checked, onChange, label, disabled }) {
12
+ return (
13
+ <label className={`ui-toggle${disabled ? ' ui-toggle--disabled' : ''}`}>
14
+ <button
15
+ type="button"
16
+ role="switch"
17
+ aria-checked={checked}
18
+ aria-label={label}
19
+ className={`ui-toggle-track${checked ? ' ui-toggle-track--on' : ''}`}
20
+ onClick={() => !disabled && onChange?.(!checked)}
21
+ disabled={disabled}
22
+ >
23
+ <span className="ui-toggle-knob" />
24
+ </button>
25
+ {label && <span className="ui-toggle-label">{label}</span>}
26
+ </label>
27
+ );
28
+ }
@@ -0,0 +1,36 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+
3
+ .ui-tooltip-trigger {
4
+ display: inline-flex;
5
+ }
6
+
7
+ .ui-tooltip {
8
+ position: fixed;
9
+ z-index: 1200;
10
+ padding: 0.3rem 0.6rem;
11
+ font-size: 0.72rem;
12
+ font-weight: 500;
13
+ color: var(--ui-text);
14
+ background: var(--ui-glass-heavy);
15
+ backdrop-filter: blur(var(--ui-glass-blur));
16
+ border: 1px solid var(--ui-border-strong);
17
+ border-radius: var(--ui-radius-sm);
18
+ box-shadow: var(--ui-shadow-lg);
19
+ white-space: nowrap;
20
+ pointer-events: none;
21
+ animation: ui-tooltip-in 0.15s var(--ui-ease);
22
+ }
23
+
24
+ @keyframes ui-tooltip-in {
25
+ from { opacity: 0; transform: translate(-50%, -100%) scale(0.95); }
26
+ to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
27
+ }
28
+
29
+ .ui-tooltip--bottom {
30
+ animation-name: ui-tooltip-in-bottom;
31
+ }
32
+
33
+ @keyframes ui-tooltip-in-bottom {
34
+ from { opacity: 0; transform: translate(-50%, 0) scale(0.95); }
35
+ to { opacity: 1; transform: translate(-50%, 0) scale(1); }
36
+ }
@@ -0,0 +1,55 @@
1
+ /** Contract: contracts/packages-ui-components/rules.md */
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import './Tooltip.css';
4
+
5
+ /**
6
+ * Hover tooltip. Wraps children in a span and positions a floating label.
7
+ * @param {string} text - Tooltip content
8
+ * @param {string} side - 'top' | 'bottom' (default: 'top')
9
+ * @param {number} delay - Show delay in ms (default: 400)
10
+ */
11
+ export default function Tooltip({ text, side = 'top', delay = 400, children }) {
12
+ const [visible, setVisible] = useState(false);
13
+ const [pos, setPos] = useState({ x: 0, y: 0 });
14
+ const ref = useRef(null);
15
+ const tipRef = useRef(null);
16
+ const timer = useRef(null);
17
+
18
+ function show() {
19
+ timer.current = setTimeout(() => {
20
+ if (!ref.current) return;
21
+ const rect = ref.current.getBoundingClientRect();
22
+ const x = rect.left + rect.width / 2;
23
+ const y = side === 'bottom' ? rect.bottom + 6 : rect.top - 6;
24
+ setPos({ x, y });
25
+ setVisible(true);
26
+ }, delay);
27
+ }
28
+
29
+ function hide() {
30
+ clearTimeout(timer.current);
31
+ setVisible(false);
32
+ }
33
+
34
+ useEffect(() => () => clearTimeout(timer.current), []);
35
+
36
+ const transform = side === 'bottom'
37
+ ? 'translate(-50%, 0)'
38
+ : 'translate(-50%, -100%)';
39
+
40
+ return (
41
+ <span ref={ref} className="ui-tooltip-trigger" onMouseEnter={show} onMouseLeave={hide} onFocus={show} onBlur={hide}>
42
+ {children}
43
+ {visible && text && (
44
+ <span
45
+ ref={tipRef}
46
+ className={`ui-tooltip ui-tooltip--${side}`}
47
+ style={{ left: pos.x, top: pos.y, transform }}
48
+ role="tooltip"
49
+ >
50
+ {text}
51
+ </span>
52
+ )}
53
+ </span>
54
+ );
55
+ }
@@ -47,7 +47,7 @@
47
47
  top: calc(100% + 6px);
48
48
  right: 0;
49
49
  min-width: 180px;
50
- background: rgba(18, 18, 24, 0.82);
50
+ background: var(--ui-glass);
51
51
  backdrop-filter: blur(var(--ui-glass-blur-heavy));
52
52
  border: 1px solid var(--ui-border);
53
53
  border-top-color: var(--ui-border-strong);
@@ -97,6 +97,23 @@
97
97
  color: var(--ui-danger);
98
98
  }
99
99
 
100
+ /* Theme toggle row */
101
+ .ui-user-menu-theme {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.5rem;
105
+ }
106
+
107
+ .ui-user-menu-theme-icon {
108
+ display: inline-flex;
109
+ color: var(--ui-text-subtle);
110
+ transition: color 0.15s;
111
+ }
112
+
113
+ .ui-user-menu-theme:hover .ui-user-menu-theme-icon {
114
+ color: var(--ui-accent);
115
+ }
116
+
100
117
  /* Separator */
101
118
  .ui-user-menu-sep {
102
119
  height: 1px;