@lokvis/ui-react 0.2.0 → 0.2.2
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/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +14 -14
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +8 -8
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +6 -6
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +7 -7
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +9 -9
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +6 -6
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +6 -6
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +7 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +13 -13
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +11 -11
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +25 -25
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +3 -3
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +22 -22
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +30 -30
- package/dist/components/Workspace.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/components/AssetPanel.tsx +263 -263
- package/src/components/Canvas.tsx +174 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +163 -163
- package/src/components/DownloadPanel.tsx +203 -203
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +170 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +136 -136
- package/src/components/ParamForm.tsx +136 -136
- package/src/components/PipelineBar.tsx +98 -98
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +148 -148
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +321 -321
- package/src/components/WorkflowTemplates.tsx +77 -77
- package/src/components/Workspace.tsx +223 -223
- package/src/css-modules.d.ts +3 -0
- package/src/index.ts +4 -0
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* 紧凑单行:左侧状态指示 + 当前工具 + 进度,右侧在线状态 + 资源统计 + 存储配额。
|
|
5
5
|
*
|
|
6
6
|
* W9.6 新增:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* - 当前选中工具名(从 selectedNode 取)
|
|
8
|
+
* - 执行进度(已完成节点数 / 总节点数)
|
|
9
|
+
* - 在线状态(navigator.onLine + 事件监听)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as React from 'react';
|
|
@@ -14,169 +14,169 @@ import { useWorkspaceStore } from '../store/index.js';
|
|
|
14
14
|
import { useWorkflowProgress } from '../hooks/useWorkflowProgress.js';
|
|
15
15
|
|
|
16
16
|
export interface StatusBarProps {
|
|
17
|
-
|
|
17
|
+
className?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** 格式化字节数为人类可读(KB/MB/GB) */
|
|
21
21
|
function formatBytes(bytes: number): string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
23
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
24
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
25
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** useOnlineStatus - 监听 navigator.onLine + online/offline 事件(W9.6) */
|
|
29
29
|
function useOnlineStatus(): boolean {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
const [online, setOnline] = React.useState<boolean>(() =>
|
|
31
|
+
typeof navigator !== 'undefined' ? navigator.onLine : true
|
|
32
|
+
);
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const onOnline = () => setOnline(true);
|
|
35
|
+
const onOffline = () => setOnline(false);
|
|
36
|
+
window.addEventListener('online', onOnline);
|
|
37
|
+
window.addEventListener('offline', onOffline);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('online', onOnline);
|
|
40
|
+
window.removeEventListener('offline', onOffline);
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
return online;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function StatusBar({ className = '' }: StatusBarProps) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
const statusMessage = useWorkspaceStore((s) => s.statusMessage);
|
|
48
|
+
const error = useWorkspaceStore((s) => s.error);
|
|
49
|
+
const running = useWorkspaceStore((s) => s.running);
|
|
50
|
+
const assets = useWorkspaceStore((s) => s.assets);
|
|
51
|
+
const capabilities = useWorkspaceStore((s) => s.capabilities);
|
|
52
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
53
|
+
const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
|
|
54
|
+
const storageUsage = useWorkspaceStore((s) => s.storageUsage);
|
|
55
|
+
const setError = useWorkspaceStore((s) => s.setError);
|
|
56
|
+
const online = useOnlineStatus();
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// W9.6 当前选中工具名
|
|
59
|
+
const selectedNode = nodes.find((n) => n.id === selectedNodeId);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// W9.6 执行进度(抽取到共享 hook,与 ProgressBar 共用,避免重复逻辑)
|
|
62
|
+
const { total: totalNodes, done: doneNodes, pct: progressPct } = useWorkflowProgress();
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// W6.7 存储配额压力:>=95% 红(临界),>=80% 琥珀(警告),其余正常
|
|
65
|
+
const ratio = storageUsage ? storageUsage.usage / storageUsage.quota : 0;
|
|
66
|
+
const storageCritical = ratio >= 0.95;
|
|
67
|
+
const storageWarning = ratio >= 0.8 && !storageCritical;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
69
|
+
return (
|
|
70
|
+
<footer
|
|
71
|
+
className={`flex h-7 shrink-0 items-center justify-between border-t border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-3 ${className}`}
|
|
72
|
+
>
|
|
73
|
+
{/* Left: Status — m5 加 aria-live,屏幕阅读器播报状态变化 */}
|
|
74
|
+
<div
|
|
75
|
+
className="flex items-center gap-2 min-w-0"
|
|
76
|
+
role="status"
|
|
77
|
+
aria-live="polite"
|
|
78
|
+
>
|
|
79
|
+
<span className="flex items-center gap-1.5">
|
|
80
|
+
<span
|
|
81
|
+
className={`h-1.5 w-1.5 rounded-full shrink-0 ${
|
|
82
|
+
error
|
|
83
|
+
? 'bg-[var(--lokvis-danger)]'
|
|
84
|
+
: running
|
|
85
|
+
? 'bg-[var(--lokvis-warning)] animate-pulse'
|
|
86
|
+
: 'bg-[var(--lokvis-success)]'
|
|
87
|
+
}`}
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
/>
|
|
90
|
+
<span className={`truncate text-[10px] ${error ? 'text-[var(--lokvis-danger)]' : 'text-[var(--lokvis-fg-muted)]'}`}>
|
|
91
|
+
{statusMessage}
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
{/* W9.6 当前工具名 */}
|
|
96
|
+
{selectedNode && (
|
|
97
|
+
<>
|
|
98
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">·</span>
|
|
99
|
+
<span className="shrink-0 font-mono text-[10px] text-[var(--lokvis-primary)] truncate max-w-[120px]">
|
|
100
|
+
{selectedNode.capability}
|
|
101
|
+
</span>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
{/* W9.6 执行进度 */}
|
|
106
|
+
{running && totalNodes > 0 && (
|
|
107
|
+
<>
|
|
108
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">·</span>
|
|
109
|
+
<span className="shrink-0 tabular-nums text-[10px] text-[var(--lokvis-warning)]">
|
|
110
|
+
{doneNodes}/{totalNodes} ({progressPct}%)
|
|
111
|
+
</span>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
{error && (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => setError(null)}
|
|
119
|
+
className="shrink-0 text-[10px] text-[var(--lokvis-danger)] underline decoration-red-300 hover:text-[var(--lokvis-danger)]"
|
|
120
|
+
>
|
|
121
|
+
dismiss
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
126
|
+
{/* Right: Stats */}
|
|
127
|
+
<div className="flex items-center gap-3 shrink-0 text-[10px] text-[var(--lokvis-fg-subtle)] tabular-nums">
|
|
128
|
+
{/* W9.6 在线状态 */}
|
|
129
|
+
<span
|
|
130
|
+
className={`flex items-center gap-1 ${online ? 'text-[var(--lokvis-success)]' : 'text-[var(--lokvis-warning)]'}`}
|
|
131
|
+
title={online ? 'Online' : 'Offline — running locally, no upload needed'}
|
|
132
|
+
aria-label={online ? 'Online' : 'Offline — files still processed locally'}
|
|
133
|
+
>
|
|
134
|
+
<span className={`h-1.5 w-1.5 rounded-full ${online ? 'bg-[var(--lokvis-success)]' : 'bg-[var(--lokvis-warning)]'}`} aria-hidden="true" />
|
|
135
|
+
{online ? 'Online' : 'Offline'}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
139
|
+
{/* W6.7 存储配额:接近上限时变色警告;m5 加 aria-label 让屏幕阅读器播报告警 */}
|
|
140
|
+
{storageUsage && (
|
|
141
|
+
<>
|
|
142
|
+
<span
|
|
143
|
+
className={`flex items-center gap-1 ${
|
|
144
|
+
storageCritical
|
|
145
|
+
? 'text-[var(--lokvis-danger)] font-semibold'
|
|
146
|
+
: storageWarning
|
|
147
|
+
? 'text-[var(--lokvis-warning)]'
|
|
148
|
+
: 'text-[var(--lokvis-fg-subtle)]'
|
|
149
|
+
}`}
|
|
150
|
+
title={
|
|
151
|
+
storageCritical
|
|
152
|
+
? `Storage almost full (${Math.round(ratio * 100)}%) — clean up to free space`
|
|
153
|
+
: storageWarning
|
|
154
|
+
? `Storage nearing limit (${Math.round(ratio * 100)}%)`
|
|
155
|
+
: `Storage: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)}`
|
|
156
|
+
}
|
|
157
|
+
aria-label={
|
|
158
|
+
storageCritical
|
|
159
|
+
? `Storage critical: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used, ${Math.round(ratio * 100)} percent. Clean up to free space.`
|
|
160
|
+
: storageWarning
|
|
161
|
+
? `Storage warning: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used, ${Math.round(ratio * 100)} percent.`
|
|
162
|
+
: `Storage: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used.`
|
|
163
|
+
}
|
|
164
|
+
aria-live={storageCritical ? 'assertive' : 'polite'}
|
|
165
|
+
>
|
|
166
|
+
{(storageCritical || storageWarning) && (
|
|
167
|
+
<span className="text-[9px]" aria-hidden="true">⚠</span>
|
|
168
|
+
)}
|
|
169
|
+
{formatBytes(storageUsage.usage)} / {formatBytes(storageUsage.quota)}
|
|
170
|
+
</span>
|
|
171
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
<span>{assets.length} asset{assets.length !== 1 ? 's' : ''}</span>
|
|
175
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
|
|
176
|
+
<span>{capabilities.length} cap{capabilities.length !== 1 ? 's' : ''}</span>
|
|
177
|
+
<span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
|
|
178
|
+
<span>{nodes.length} step{nodes.length !== 1 ? 's' : ''}</span>
|
|
179
|
+
</div>
|
|
180
|
+
</footer>
|
|
181
|
+
);
|
|
182
182
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* 与 `tokens.css` 的 `.dark` / `.light` 类双触发机制对齐。
|
|
6
6
|
*
|
|
7
7
|
* 交互:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* - 左键单击:在 light / dark 之间切换(基于当前 resolvedTheme 决定下一态)
|
|
9
|
+
* - 右键单击:弹出菜单(light / dark / system 三态)
|
|
10
|
+
* - ESC:关闭菜单
|
|
11
|
+
* - 点击菜单外部:关闭菜单
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import * as React from 'react';
|
|
@@ -16,107 +16,107 @@ import { Icon } from '@lokvis/ui-core';
|
|
|
16
16
|
import { useTheme, type ThemeMode } from '../hooks/useTheme.js';
|
|
17
17
|
|
|
18
18
|
export interface ThemeToggleProps {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
className?: string;
|
|
20
|
+
/** 是否显示文字标签(默认仅图标) */
|
|
21
|
+
showLabel?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function ThemeToggle({ className = '', showLabel = false }: ThemeToggleProps) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const { theme, resolvedTheme, toggleTheme, setTheme } = useTheme();
|
|
26
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
27
|
+
const btnRef = React.useRef<HTMLButtonElement>(null);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
// 点击外部关闭菜单 + ESC 关闭
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!menuOpen) return;
|
|
32
|
+
const onDown = (e: MouseEvent) => {
|
|
33
|
+
// e.target 可能是任意 EventTarget;用 instanceof Node 类型守卫收窄,
|
|
34
|
+
// 避免 as 断言(与 WorkflowEditor 等处统一)
|
|
35
|
+
if (e.target instanceof Node && btnRef.current && !btnRef.current.contains(e.target)) {
|
|
36
|
+
setMenuOpen(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const onKey = (e: KeyboardEvent) => {
|
|
40
|
+
if (e.key === 'Escape') setMenuOpen(false);
|
|
41
|
+
};
|
|
42
|
+
document.addEventListener('mousedown', onDown);
|
|
43
|
+
document.addEventListener('keydown', onKey);
|
|
44
|
+
return () => {
|
|
45
|
+
document.removeEventListener('mousedown', onDown);
|
|
46
|
+
document.removeEventListener('keydown', onKey);
|
|
47
|
+
};
|
|
48
|
+
}, [menuOpen]);
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
const isDark = resolvedTheme === 'dark';
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
const icon = isDark ? (
|
|
53
|
+
<Icon size={14} strokeWidth={2}>
|
|
54
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
55
|
+
</Icon>
|
|
56
|
+
) : (
|
|
57
|
+
<Icon size={14} strokeWidth={2}>
|
|
58
|
+
<circle cx="12" cy="12" r="4" />
|
|
59
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
60
|
+
</Icon>
|
|
61
|
+
);
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
const label = theme === null ? 'System' : theme === 'dark' ? 'Dark' : 'Light';
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
return (
|
|
66
|
+
<div className={`relative ${className}`}>
|
|
67
|
+
<button
|
|
68
|
+
ref={btnRef}
|
|
69
|
+
type="button"
|
|
70
|
+
aria-haspopup="menu"
|
|
71
|
+
aria-expanded={menuOpen}
|
|
72
|
+
onClick={() => toggleTheme()}
|
|
73
|
+
onContextMenu={(e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
setMenuOpen((v) => !v);
|
|
76
|
+
}}
|
|
77
|
+
aria-label={`Theme: ${label}. Click to toggle, right-click for options.`}
|
|
78
|
+
title={`Theme: ${label} (right-click for options)`}
|
|
79
|
+
className="flex h-8 w-8 items-center justify-center rounded-md text-[var(--lokvis-fg-muted)] transition-colors hover:bg-[var(--lokvis-surface-muted)] hover:text-[var(--lokvis-fg-muted)]"
|
|
80
|
+
>
|
|
81
|
+
{icon}
|
|
82
|
+
{showLabel && <span className="ml-1 text-xs">{label}</span>}
|
|
83
|
+
</button>
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
85
|
+
{menuOpen && (
|
|
86
|
+
<div
|
|
87
|
+
role="menu"
|
|
88
|
+
className="absolute right-0 top-full z-50 mt-1 w-32 overflow-hidden rounded-md border border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] py-1 shadow-lg"
|
|
89
|
+
>
|
|
90
|
+
{(['light', 'dark', null] as Array<ThemeMode | null>).map((mode) => {
|
|
91
|
+
const isActive = theme === mode;
|
|
92
|
+
const text = mode === null ? 'System' : mode === 'dark' ? 'Dark' : 'Light';
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
key={mode ?? 'system'}
|
|
96
|
+
type="button"
|
|
97
|
+
role="menuitemradio"
|
|
98
|
+
aria-checked={isActive}
|
|
99
|
+
onClick={() => {
|
|
100
|
+
setTheme(mode);
|
|
101
|
+
setMenuOpen(false);
|
|
102
|
+
}}
|
|
103
|
+
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-xs transition-colors ${
|
|
104
|
+
isActive
|
|
105
|
+
? 'bg-[var(--lokvis-primary)]/10 text-[var(--lokvis-primary)]'
|
|
106
|
+
: 'text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-surface-muted)]'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
<span>{text}</span>
|
|
110
|
+
{isActive && (
|
|
111
|
+
<Icon size={12} strokeWidth={3}>
|
|
112
|
+
<path d="M5 13l4 4L19 7" />
|
|
113
|
+
</Icon>
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
122
|
}
|