@lokvis/ui-react 0.2.1 → 0.4.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 (165) hide show
  1. package/dist/components/AssetPanel.d.ts.map +1 -1
  2. package/dist/components/AssetPanel.js +18 -62
  3. package/dist/components/AssetPanel.js.map +1 -1
  4. package/dist/components/Canvas.d.ts +4 -4
  5. package/dist/components/Canvas.d.ts.map +1 -1
  6. package/dist/components/Canvas.js +9 -9
  7. package/dist/components/Canvas.js.map +1 -1
  8. package/dist/components/CommandPalette.d.ts +7 -7
  9. package/dist/components/CommandPalette.d.ts.map +1 -1
  10. package/dist/components/CommandPalette.js +10 -10
  11. package/dist/components/CommandPalette.js.map +1 -1
  12. package/dist/components/CompareSlider.d.ts +9 -8
  13. package/dist/components/CompareSlider.d.ts.map +1 -1
  14. package/dist/components/CompareSlider.js +10 -9
  15. package/dist/components/CompareSlider.js.map +1 -1
  16. package/dist/components/DownloadPanel.d.ts +3 -3
  17. package/dist/components/DownloadPanel.d.ts.map +1 -1
  18. package/dist/components/DownloadPanel.js +10 -17
  19. package/dist/components/DownloadPanel.js.map +1 -1
  20. package/dist/components/ErrorBanner.d.ts +2 -2
  21. package/dist/components/ErrorBanner.d.ts.map +1 -1
  22. package/dist/components/ErrorBanner.js +3 -3
  23. package/dist/components/ErrorBanner.js.map +1 -1
  24. package/dist/components/ErrorBoundary.d.ts +40 -0
  25. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  26. package/dist/components/ErrorBoundary.js +52 -0
  27. package/dist/components/ErrorBoundary.js.map +1 -0
  28. package/dist/components/ExifPanel.d.ts +1 -1
  29. package/dist/components/ExifPanel.d.ts.map +1 -1
  30. package/dist/components/ExifPanel.js +2 -2
  31. package/dist/components/ExifPanel.js.map +1 -1
  32. package/dist/components/GlobalDropzone.d.ts +5 -5
  33. package/dist/components/GlobalDropzone.d.ts.map +1 -1
  34. package/dist/components/GlobalDropzone.js +15 -8
  35. package/dist/components/GlobalDropzone.js.map +1 -1
  36. package/dist/components/HistoryPanel.d.ts +2 -2
  37. package/dist/components/HistoryPanel.d.ts.map +1 -1
  38. package/dist/components/HistoryPanel.js +23 -23
  39. package/dist/components/HistoryPanel.js.map +1 -1
  40. package/dist/components/Inspector.d.ts.map +1 -1
  41. package/dist/components/Inspector.js +13 -10
  42. package/dist/components/Inspector.js.map +1 -1
  43. package/dist/components/ParamForm.d.ts.map +1 -1
  44. package/dist/components/ParamForm.js +10 -7
  45. package/dist/components/ParamForm.js.map +1 -1
  46. package/dist/components/PipelineBar.d.ts.map +1 -1
  47. package/dist/components/PipelineBar.js +19 -28
  48. package/dist/components/PipelineBar.js.map +1 -1
  49. package/dist/components/ProgressBar.d.ts +4 -4
  50. package/dist/components/ProgressBar.d.ts.map +1 -1
  51. package/dist/components/ProgressBar.js +8 -8
  52. package/dist/components/ProgressBar.js.map +1 -1
  53. package/dist/components/StatusBar.d.ts +3 -3
  54. package/dist/components/StatusBar.d.ts.map +1 -1
  55. package/dist/components/StatusBar.js +12 -21
  56. package/dist/components/StatusBar.js.map +1 -1
  57. package/dist/components/StatusDot.d.ts +12 -0
  58. package/dist/components/StatusDot.d.ts.map +1 -0
  59. package/dist/components/StatusDot.js +15 -0
  60. package/dist/components/StatusDot.js.map +1 -0
  61. package/dist/components/ThemeToggle.d.ts +4 -4
  62. package/dist/components/ThemeToggle.d.ts.map +1 -1
  63. package/dist/components/ThemeToggle.js +7 -7
  64. package/dist/components/ThemeToggle.js.map +1 -1
  65. package/dist/components/ToolRunner.d.ts +53 -0
  66. package/dist/components/ToolRunner.d.ts.map +1 -0
  67. package/dist/components/ToolRunner.js +142 -0
  68. package/dist/components/ToolRunner.js.map +1 -0
  69. package/dist/components/Toolbar.d.ts.map +1 -1
  70. package/dist/components/Toolbar.js +3 -3
  71. package/dist/components/Toolbar.js.map +1 -1
  72. package/dist/components/WorkflowEditor.d.ts +6 -6
  73. package/dist/components/WorkflowEditor.d.ts.map +1 -1
  74. package/dist/components/WorkflowEditor.js +40 -49
  75. package/dist/components/WorkflowEditor.js.map +1 -1
  76. package/dist/components/WorkflowTemplates.d.ts +2 -2
  77. package/dist/components/WorkflowTemplates.d.ts.map +1 -1
  78. package/dist/components/WorkflowTemplates.js +17 -7
  79. package/dist/components/WorkflowTemplates.js.map +1 -1
  80. package/dist/components/Workspace.d.ts +45 -23
  81. package/dist/components/Workspace.d.ts.map +1 -1
  82. package/dist/components/Workspace.js +87 -40
  83. package/dist/components/Workspace.js.map +1 -1
  84. package/dist/hooks/useCustomPresets.d.ts +98 -0
  85. package/dist/hooks/useCustomPresets.d.ts.map +1 -0
  86. package/dist/hooks/useCustomPresets.js +210 -0
  87. package/dist/hooks/useCustomPresets.js.map +1 -0
  88. package/dist/hooks/useLokvis.d.ts +19 -1
  89. package/dist/hooks/useLokvis.d.ts.map +1 -1
  90. package/dist/hooks/useLokvis.js +30 -9
  91. package/dist/hooks/useLokvis.js.map +1 -1
  92. package/dist/index.d.ts +4 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +6 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/store/assets-slice.d.ts +8 -1
  97. package/dist/store/assets-slice.d.ts.map +1 -1
  98. package/dist/store/assets-slice.js +101 -54
  99. package/dist/store/assets-slice.js.map +1 -1
  100. package/dist/store/capabilities-slice.d.ts +1 -1
  101. package/dist/store/capabilities-slice.d.ts.map +1 -1
  102. package/dist/store/capabilities-slice.js +8 -1
  103. package/dist/store/capabilities-slice.js.map +1 -1
  104. package/dist/store/index.d.ts +1 -1
  105. package/dist/store/index.d.ts.map +1 -1
  106. package/dist/store/index.js +1 -1
  107. package/dist/store/index.js.map +1 -1
  108. package/dist/store/runtime-slice.d.ts.map +1 -1
  109. package/dist/store/runtime-slice.js +22 -16
  110. package/dist/store/runtime-slice.js.map +1 -1
  111. package/dist/store/subscribe-utils.d.ts +45 -0
  112. package/dist/store/subscribe-utils.d.ts.map +1 -0
  113. package/dist/store/subscribe-utils.js +69 -0
  114. package/dist/store/subscribe-utils.js.map +1 -0
  115. package/dist/store/types.d.ts +19 -11
  116. package/dist/store/types.d.ts.map +1 -1
  117. package/dist/store/types.js +0 -10
  118. package/dist/store/types.js.map +1 -1
  119. package/dist/store/workflow-slice.d.ts +1 -1
  120. package/dist/store/workflow-slice.d.ts.map +1 -1
  121. package/dist/store/workflow-slice.js +19 -51
  122. package/dist/store/workflow-slice.js.map +1 -1
  123. package/dist/utils.d.ts +24 -0
  124. package/dist/utils.d.ts.map +1 -0
  125. package/dist/utils.js +31 -0
  126. package/dist/utils.js.map +1 -0
  127. package/package.json +7 -6
  128. package/src/components/AssetPanel.tsx +224 -264
  129. package/src/components/Canvas.tsx +178 -174
  130. package/src/components/CommandPalette.tsx +242 -242
  131. package/src/components/CompareSlider.tsx +168 -165
  132. package/src/components/DownloadPanel.tsx +201 -207
  133. package/src/components/ErrorBanner.tsx +64 -64
  134. package/src/components/ErrorBoundary.tsx +101 -0
  135. package/src/components/ExifPanel.tsx +154 -154
  136. package/src/components/GlobalDropzone.tsx +175 -170
  137. package/src/components/HistoryPanel.tsx +219 -219
  138. package/src/components/Inspector.tsx +145 -137
  139. package/src/components/ParamForm.tsx +140 -137
  140. package/src/components/PipelineBar.tsx +97 -104
  141. package/src/components/ProgressBar.tsx +78 -78
  142. package/src/components/StatusBar.tsx +145 -152
  143. package/src/components/StatusDot.tsx +27 -0
  144. package/src/components/ThemeToggle.tsx +98 -98
  145. package/src/components/ToolRunner.tsx +352 -0
  146. package/src/components/Toolbar.tsx +65 -65
  147. package/src/components/WorkflowEditor.tsx +333 -326
  148. package/src/components/WorkflowTemplates.tsx +101 -78
  149. package/src/components/Workspace.tsx +310 -223
  150. package/src/css-modules.d.ts +3 -0
  151. package/src/hooks/useCustomPresets.ts +284 -0
  152. package/src/hooks/useLokvis.ts +43 -10
  153. package/src/index.ts +7 -0
  154. package/src/store/assets-slice.ts +106 -52
  155. package/src/store/capabilities-slice.ts +10 -2
  156. package/src/store/index.ts +1 -1
  157. package/src/store/runtime-slice.ts +20 -15
  158. package/src/store/subscribe-utils.ts +82 -0
  159. package/src/store/types.ts +19 -12
  160. package/src/store/workflow-slice.ts +19 -53
  161. package/src/utils.ts +41 -0
  162. package/dist/store.d.ts +0 -76
  163. package/dist/store.d.ts.map +0 -1
  164. package/dist/store.js +0 -217
  165. package/dist/store.js.map +0 -1
@@ -4,179 +4,172 @@
4
4
  * 紧凑单行:左侧状态指示 + 当前工具 + 进度,右侧在线状态 + 资源统计 + 存储配额。
5
5
  *
6
6
  * W9.6 新增:
7
- * - 当前选中工具名(从 selectedNode 取)
8
- * - 执行进度(已完成节点数 / 总节点数)
9
- * - 在线状态(navigator.onLine + 事件监听)
7
+ * - 当前选中工具名(从 selectedNode 取)
8
+ * - 执行进度(已完成节点数 / 总节点数)
9
+ * - 在线状态(navigator.onLine + 事件监听)
10
10
  */
11
11
 
12
12
  import * as React from 'react';
13
13
  import { useWorkspaceStore } from '../store/index.js';
14
14
  import { useWorkflowProgress } from '../hooks/useWorkflowProgress.js';
15
+ import { formatBytes } from '../utils.js';
15
16
 
16
17
  export interface StatusBarProps {
17
- className?: string;
18
- }
19
-
20
- /** 格式化字节数为人类可读(KB/MB/GB) */
21
- function formatBytes(bytes: number): string {
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`;
18
+ className?: string;
26
19
  }
27
20
 
28
21
  /** useOnlineStatus - 监听 navigator.onLine + online/offline 事件(W9.6) */
29
22
  function useOnlineStatus(): boolean {
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;
23
+ const [online, setOnline] = React.useState<boolean>(() =>
24
+ typeof navigator !== 'undefined' ? navigator.onLine : true
25
+ );
26
+ React.useEffect(() => {
27
+ const onOnline = () => setOnline(true);
28
+ const onOffline = () => setOnline(false);
29
+ window.addEventListener('online', onOnline);
30
+ window.addEventListener('offline', onOffline);
31
+ return () => {
32
+ window.removeEventListener('online', onOnline);
33
+ window.removeEventListener('offline', onOffline);
34
+ };
35
+ }, []);
36
+ return online;
44
37
  }
45
38
 
46
39
  export function StatusBar({ className = '' }: StatusBarProps) {
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();
40
+ const statusMessage = useWorkspaceStore((s) => s.statusMessage);
41
+ const error = useWorkspaceStore((s) => s.error);
42
+ const running = useWorkspaceStore((s) => s.running);
43
+ const assets = useWorkspaceStore((s) => s.assets);
44
+ const capabilities = useWorkspaceStore((s) => s.capabilities);
45
+ const nodes = useWorkspaceStore((s) => s.nodes);
46
+ const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
47
+ const storageUsage = useWorkspaceStore((s) => s.storageUsage);
48
+ const setError = useWorkspaceStore((s) => s.setError);
49
+ const online = useOnlineStatus();
57
50
 
58
- // W9.6 当前选中工具名
59
- const selectedNode = nodes.find((n) => n.id === selectedNodeId);
51
+ // W9.6 当前选中工具名
52
+ const selectedNode = nodes.find((n) => n.id === selectedNodeId);
60
53
 
61
- // W9.6 执行进度(抽取到共享 hook,与 ProgressBar 共用,避免重复逻辑)
62
- const { total: totalNodes, done: doneNodes, pct: progressPct } = useWorkflowProgress();
54
+ // W9.6 执行进度(抽取到共享 hook,与 ProgressBar 共用,避免重复逻辑)
55
+ const { total: totalNodes, done: doneNodes, pct: progressPct } = useWorkflowProgress();
63
56
 
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;
57
+ // W6.7 存储配额压力:>=95% 红(临界),>=80% 琥珀(警告),其余正常
58
+ const ratio = storageUsage ? storageUsage.usage / storageUsage.quota : 0;
59
+ const storageCritical = ratio >= 0.95;
60
+ const storageWarning = ratio >= 0.8 && !storageCritical;
68
61
 
69
- return (
70
- <footer
71
- className={`flex h-7 shrink-0 items-center justify-between border-t border-zinc-200 bg-zinc-50 px-3 dark:border-zinc-800 dark:bg-zinc-950 ${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-red-500'
84
- : running
85
- ? 'bg-amber-500 animate-pulse'
86
- : 'bg-emerald-500'
87
- }`}
88
- aria-hidden="true"
89
- />
90
- <span className={`truncate text-[10px] ${error ? 'text-red-500' : 'text-zinc-500'}`}>
91
- {statusMessage}
92
- </span>
93
- </span>
62
+ return (
63
+ <footer
64
+ className={`flex h-[var(--lokvis-statusbar-h)] shrink-0 items-center justify-between border-t border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-3 ${className}`}
65
+ >
66
+ {/* Left: Status — m5 加 aria-live,屏幕阅读器播报状态变化 */}
67
+ <div
68
+ className="flex items-center gap-2 min-w-0"
69
+ role="status"
70
+ aria-live="polite"
71
+ >
72
+ <span className="flex items-center gap-1.5">
73
+ <span
74
+ className={`h-1.5 w-1.5 rounded-full shrink-0 ${
75
+ error
76
+ ? 'bg-[var(--lokvis-danger)]'
77
+ : running
78
+ ? 'bg-[var(--lokvis-warning)] animate-pulse'
79
+ : 'bg-[var(--lokvis-success)]'
80
+ }`}
81
+ aria-hidden="true"
82
+ />
83
+ <span className={`truncate text-[10px] ${error ? 'text-[var(--lokvis-danger)]' : 'text-[var(--lokvis-fg-muted)]'}`}>
84
+ {statusMessage}
85
+ </span>
86
+ </span>
94
87
 
95
- {/* W9.6 当前工具名 */}
96
- {selectedNode && (
97
- <>
98
- <span className="text-zinc-300 dark:text-zinc-700" aria-hidden="true">·</span>
99
- <span className="shrink-0 font-mono text-[10px] text-indigo-500 dark:text-indigo-400 truncate max-w-[120px]">
100
- {selectedNode.capability}
101
- </span>
102
- </>
103
- )}
88
+ {/* W9.6 当前工具名 */}
89
+ {selectedNode && (
90
+ <>
91
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">·</span>
92
+ <span className="shrink-0 font-mono text-[10px] text-[var(--lokvis-primary)] truncate max-w-[120px]">
93
+ {selectedNode.capability}
94
+ </span>
95
+ </>
96
+ )}
104
97
 
105
- {/* W9.6 执行进度 */}
106
- {running && totalNodes > 0 && (
107
- <>
108
- <span className="text-zinc-300 dark:text-zinc-700" aria-hidden="true">·</span>
109
- <span className="shrink-0 tabular-nums text-[10px] text-amber-600 dark:text-amber-400">
110
- {doneNodes}/{totalNodes} ({progressPct}%)
111
- </span>
112
- </>
113
- )}
98
+ {/* W9.6 执行进度 */}
99
+ {running && totalNodes > 0 && (
100
+ <>
101
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">·</span>
102
+ <span className="shrink-0 tabular-nums text-[10px] text-[var(--lokvis-warning)]">
103
+ {doneNodes}/{totalNodes} ({progressPct}%)
104
+ </span>
105
+ </>
106
+ )}
114
107
 
115
- {error && (
116
- <button
117
- type="button"
118
- onClick={() => setError(null)}
119
- className="shrink-0 text-[10px] text-red-500 underline decoration-red-300 hover:text-red-600 dark:decoration-red-800"
120
- >
121
- dismiss
122
- </button>
123
- )}
124
- </div>
108
+ {error && (
109
+ <button
110
+ type="button"
111
+ onClick={() => setError(null)}
112
+ className="shrink-0 text-[10px] text-[var(--lokvis-danger)] underline decoration-[var(--lokvis-danger)]/40 hover:text-[var(--lokvis-danger)]"
113
+ >
114
+ dismiss
115
+ </button>
116
+ )}
117
+ </div>
125
118
 
126
- {/* Right: Stats */}
127
- <div className="flex items-center gap-3 shrink-0 text-[10px] text-zinc-400 tabular-nums">
128
- {/* W9.6 在线状态 */}
129
- <span
130
- className={`flex items-center gap-1 ${online ? 'text-emerald-500' : 'text-amber-500'}`}
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-emerald-500' : 'bg-amber-500'}`} aria-hidden="true" />
135
- {online ? 'Online' : 'Offline'}
136
- </span>
137
- <span className="text-zinc-300 dark:text-zinc-700" aria-hidden="true">|</span>
119
+ {/* Right: Stats */}
120
+ <div className="flex items-center gap-3 shrink-0 text-[10px] text-[var(--lokvis-fg-subtle)] tabular-nums">
121
+ {/* W9.6 在线状态 */}
122
+ <span
123
+ className={`flex items-center gap-1 ${online ? 'text-[var(--lokvis-success)]' : 'text-[var(--lokvis-warning)]'}`}
124
+ title={online ? 'Online' : 'Offline — running locally, no upload needed'}
125
+ aria-label={online ? 'Online' : 'Offline — files still processed locally'}
126
+ >
127
+ <span className={`h-1.5 w-1.5 rounded-full ${online ? 'bg-[var(--lokvis-success)]' : 'bg-[var(--lokvis-warning)]'}`} aria-hidden="true" />
128
+ {online ? 'Online' : 'Offline'}
129
+ </span>
130
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
138
131
 
139
- {/* W6.7 存储配额:接近上限时变色警告;m5 加 aria-label 让屏幕阅读器播报告警 */}
140
- {storageUsage && (
141
- <>
142
- <span
143
- className={`flex items-center gap-1 ${
144
- storageCritical
145
- ? 'text-red-500 font-semibold'
146
- : storageWarning
147
- ? 'text-amber-500'
148
- : 'text-zinc-400'
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-zinc-300 dark:text-zinc-700" aria-hidden="true">|</span>
172
- </>
173
- )}
174
- <span>{assets.length} asset{assets.length !== 1 ? 's' : ''}</span>
175
- <span className="text-zinc-300 dark:text-zinc-700" aria-hidden="true">|</span>
176
- <span>{capabilities.length} cap{capabilities.length !== 1 ? 's' : ''}</span>
177
- <span className="text-zinc-300 dark:text-zinc-700" aria-hidden="true">|</span>
178
- <span>{nodes.length} step{nodes.length !== 1 ? 's' : ''}</span>
179
- </div>
180
- </footer>
181
- );
132
+ {/* W6.7 存储配额:接近上限时变色警告;m5 加 aria-label 让屏幕阅读器播报告警 */}
133
+ {storageUsage && (
134
+ <>
135
+ <span
136
+ className={`flex items-center gap-1 ${
137
+ storageCritical
138
+ ? 'text-[var(--lokvis-danger)] font-semibold'
139
+ : storageWarning
140
+ ? 'text-[var(--lokvis-warning)]'
141
+ : 'text-[var(--lokvis-fg-subtle)]'
142
+ }`}
143
+ title={
144
+ storageCritical
145
+ ? `Storage almost full (${Math.round(ratio * 100)}%) — clean up to free space`
146
+ : storageWarning
147
+ ? `Storage nearing limit (${Math.round(ratio * 100)}%)`
148
+ : `Storage: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)}`
149
+ }
150
+ aria-label={
151
+ storageCritical
152
+ ? `Storage critical: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used, ${Math.round(ratio * 100)} percent. Clean up to free space.`
153
+ : storageWarning
154
+ ? `Storage warning: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used, ${Math.round(ratio * 100)} percent.`
155
+ : `Storage: ${formatBytes(storageUsage.usage)} of ${formatBytes(storageUsage.quota)} used.`
156
+ }
157
+ aria-live={storageCritical ? 'assertive' : 'polite'}
158
+ >
159
+ {(storageCritical || storageWarning) && (
160
+ <span className="text-[9px]" aria-hidden="true">⚠</span>
161
+ )}
162
+ {formatBytes(storageUsage.usage)} / {formatBytes(storageUsage.quota)}
163
+ </span>
164
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
165
+ </>
166
+ )}
167
+ <span>{assets.length} asset{assets.length !== 1 ? 's' : ''}</span>
168
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
169
+ <span>{capabilities.length} cap{capabilities.length !== 1 ? 's' : ''}</span>
170
+ <span className="text-[var(--lokvis-fg-subtle)]" aria-hidden="true">|</span>
171
+ <span>{nodes.length} step{nodes.length !== 1 ? 's' : ''}</span>
172
+ </div>
173
+ </footer>
174
+ );
182
175
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * StatusDot - 节点状态指示圆点
3
+ *
4
+ * 根据 status 显示不同颜色,running 时带 pulse 动画。
5
+ * PipelineBar 与 WorkflowEditor 共享此组件。
6
+ */
7
+ export interface StatusDotProps {
8
+ /** 节点状态(pending/running/success/failed/idle) */
9
+ status: string;
10
+ }
11
+
12
+ export function StatusDot({ status }: StatusDotProps) {
13
+ const color =
14
+ status === 'running'
15
+ ? 'bg-[var(--lokvis-warning)]'
16
+ : status === 'success'
17
+ ? 'bg-[var(--lokvis-success)]'
18
+ : status === 'failed'
19
+ ? 'bg-[var(--lokvis-danger)]'
20
+ : status === 'pending'
21
+ ? 'bg-[var(--lokvis-fg-subtle)]'
22
+ : 'bg-[var(--lokvis-fg-subtle)]';
23
+
24
+ const animate = status === 'running' ? 'animate-pulse' : '';
25
+
26
+ return <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${color} ${animate}`} />;
27
+ }
@@ -5,10 +5,10 @@
5
5
  * 与 `tokens.css` 的 `.dark` / `.light` 类双触发机制对齐。
6
6
  *
7
7
  * 交互:
8
- * - 左键单击:在 light / dark 之间切换(基于当前 resolvedTheme 决定下一态)
9
- * - 右键单击:弹出菜单(light / dark / system 三态)
10
- * - ESC:关闭菜单
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
- className?: string;
20
- /** 是否显示文字标签(默认仅图标) */
21
- showLabel?: boolean;
19
+ className?: string;
20
+ /** 是否显示文字标签(默认仅图标) */
21
+ showLabel?: boolean;
22
22
  }
23
23
 
24
24
  export function ThemeToggle({ className = '', showLabel = false }: ThemeToggleProps) {
25
- const { theme, resolvedTheme, toggleTheme, setTheme } = useTheme();
26
- const [menuOpen, setMenuOpen] = React.useState(false);
27
- const btnRef = React.useRef<HTMLButtonElement>(null);
25
+ const { theme, resolvedTheme, toggleTheme, setTheme } = useTheme();
26
+ const [menuOpen, setMenuOpen] = React.useState(false);
27
+ const btnRef = React.useRef<HTMLButtonElement>(null);
28
28
 
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]);
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
- const isDark = resolvedTheme === 'dark';
50
+ const isDark = resolvedTheme === 'dark';
51
51
 
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
- );
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
- const label = theme === null ? 'System' : theme === 'dark' ? 'Dark' : 'Light';
63
+ const label = theme === null ? 'System' : theme === 'dark' ? 'Dark' : 'Light';
64
64
 
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-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
80
- >
81
- {icon}
82
- {showLabel && <span className="ml-1 text-xs">{label}</span>}
83
- </button>
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
- {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-zinc-200 bg-white py-1 shadow-lg dark:border-zinc-800 dark:bg-zinc-900"
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-indigo-50 text-indigo-700 dark:bg-indigo-950/30 dark:text-indigo-300'
106
- : 'text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800'
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
- );
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-[var(--lokvis-elevation-2)]"
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
  }