@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
@@ -0,0 +1,352 @@
1
+ /**
2
+ * ToolRunner - 单工具页组合组件
3
+ *
4
+ * 为单工具操作场景(压缩、裁剪、格式转换等)提供一站式 UI:
5
+ * 上传 → 调参 → 处理 → 对比 → 下载。
6
+ *
7
+ * 不依赖 Workspace Store(zustand),通过 engine prop 注入处理函数,
8
+ * 可在任何 React 应用中独立使用。
9
+ *
10
+ * 五层架构:UI 层不直接依赖 Engine 层,engine 函数由消费方传入。
11
+ * 消费方需自行实现按 capability.name 路由到对应 Blob↔Blob 操作的 wrapper
12
+ * (engine-image 已不再提供 runTool 统一入口,各 operation 需直接组合调用)。
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import { ToolRunner } from '@lokvis/ui-react';
17
+ * import { IMAGE_COMPRESS, compress } from '@lokvis/engine-image';
18
+ *
19
+ * <ToolRunner
20
+ * capability={IMAGE_COMPRESS}
21
+ * engine={(name, input, params, opts) => compress(input, params, opts?.signal)}
22
+ * onOpenInWorkspace={(state) => { ... }}
23
+ * />
24
+ * ```
25
+ */
26
+
27
+ import * as React from 'react';
28
+ import type { Capability } from '@lokvis/schema';
29
+ import { Button, Card } from '@lokvis/ui-core';
30
+ import { ParamForm } from './ParamForm.js';
31
+
32
+ type EngineFn = (
33
+ toolName: string,
34
+ input: Blob,
35
+ params: Record<string, unknown>,
36
+ options?: { signal?: AbortSignal; onProgress?: (p: number) => void },
37
+ ) => Promise<Blob>;
38
+
39
+ export interface ToolRunnerProps {
40
+ /** 工具能力定义(决定参数表单和输入类型) */
41
+ capability: Capability;
42
+ /** 处理函数(按 capability.name 路由到对应 Blob↔Blob 操作的 wrapper) */
43
+ engine: EngineFn;
44
+ /** 在 Workspace 中打开的回调(携带当前状态) */
45
+ onOpenInWorkspace?: (state: {
46
+ inputBlob: Blob;
47
+ outputBlob: Blob | null;
48
+ params: Record<string, unknown>;
49
+ }) => void;
50
+ className?: string;
51
+ }
52
+
53
+ /**
54
+ * ToolRunner - 单工具操作组件。
55
+ *
56
+ * 内部组合:上传区 + 参数表单 + 预览/对比 + 进度 + 下载。
57
+ * 所有状态自管理,不依赖外部 store。
58
+ */
59
+ export function ToolRunner({
60
+ capability,
61
+ engine,
62
+ onOpenInWorkspace,
63
+ className = '',
64
+ }: ToolRunnerProps) {
65
+ const [inputBlob, setInputBlob] = React.useState<Blob | null>(null);
66
+ const [inputUrl, setInputUrl] = React.useState<string | null>(null);
67
+ const [outputBlob, setOutputBlob] = React.useState<Blob | null>(null);
68
+ const [outputUrl, setOutputUrl] = React.useState<string | null>(null);
69
+ const [params, setParams] = React.useState<Record<string, unknown>>(() => {
70
+ const defaults: Record<string, unknown> = {};
71
+ for (const p of capability.params) {
72
+ if (p.default !== undefined) defaults[p.name] = p.default;
73
+ }
74
+ return defaults;
75
+ });
76
+ const [progress, setProgress] = React.useState(0);
77
+ const [processing, setProcessing] = React.useState(false);
78
+ const [error, setError] = React.useState<string | null>(null);
79
+ const [dragging, setDragging] = React.useState(false);
80
+ const abortRef = React.useRef<AbortController | null>(null);
81
+
82
+ // 清理 ObjectURL
83
+ React.useEffect(() => {
84
+ return () => {
85
+ if (inputUrl) URL.revokeObjectURL(inputUrl);
86
+ if (outputUrl) URL.revokeObjectURL(outputUrl);
87
+ };
88
+ }, [inputUrl, outputUrl]);
89
+
90
+ // W21.6: unmount 时 abort 进行中的处理,防止后台泄漏
91
+ // (engine 函数持有 Worker/Canvas 资源,卸载后继续运行是浪费 + 潜在内存泄漏)
92
+ React.useEffect(() => {
93
+ return () => {
94
+ if (abortRef.current) {
95
+ abortRef.current.abort();
96
+ abortRef.current = null;
97
+ }
98
+ };
99
+ }, []);
100
+
101
+ const accept = capability.inputTypes.join(',');
102
+
103
+ function handleFiles(files: FileList | File[] | null) {
104
+ if (!files || files.length === 0) return;
105
+ const file = files[0];
106
+ if (!file) return;
107
+
108
+ setInputBlob(file);
109
+ if (inputUrl) URL.revokeObjectURL(inputUrl);
110
+ setInputUrl(URL.createObjectURL(file));
111
+
112
+ // 重置输出
113
+ setOutputBlob(null);
114
+ if (outputUrl) {
115
+ URL.revokeObjectURL(outputUrl);
116
+ setOutputUrl(null);
117
+ }
118
+ }
119
+
120
+ async function handleProcess() {
121
+ if (!inputBlob) return;
122
+ setProcessing(true);
123
+ setProgress(0);
124
+ setError(null);
125
+
126
+ abortRef.current = new AbortController();
127
+ try {
128
+ const result = await engine(capability.name, inputBlob, params, {
129
+ signal: abortRef.current.signal,
130
+ onProgress: setProgress,
131
+ });
132
+ setOutputBlob(result);
133
+ if (outputUrl) URL.revokeObjectURL(outputUrl);
134
+ setOutputUrl(URL.createObjectURL(result));
135
+ } catch (err) {
136
+ if (err instanceof Error && err.name === 'AbortError') return;
137
+ setError(err instanceof Error ? err.message : String(err));
138
+ } finally {
139
+ setProcessing(false);
140
+ abortRef.current = null;
141
+ }
142
+ }
143
+
144
+ function handleCancel() {
145
+ abortRef.current?.abort();
146
+ }
147
+
148
+ const downloadName = outputBlob
149
+ ? `output.${outputBlob.type.split('/')[1] ?? 'bin'}`
150
+ : 'output';
151
+
152
+ return (
153
+ <div className={`flex flex-col gap-6 ${className}`}>
154
+ {/* Upload zone */}
155
+ {!inputBlob && (
156
+ <label
157
+ className={`block cursor-pointer rounded-xl border-2 border-dashed p-10 text-center transition-all ${
158
+ dragging
159
+ ? 'border-[var(--lokvis-primary)] bg-[var(--lokvis-primary)]/10'
160
+ : 'border-[var(--lokvis-border)] hover:border-[var(--lokvis-primary)]/50 hover:bg-[var(--lokvis-surface-muted)]'
161
+ }`}
162
+ onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
163
+ onDragLeave={() => setDragging(false)}
164
+ onDrop={(e) => {
165
+ e.preventDefault();
166
+ setDragging(false);
167
+ handleFiles(e.dataTransfer.files);
168
+ }}
169
+ >
170
+ <input
171
+ type="file"
172
+ accept={accept}
173
+ className="hidden"
174
+ onChange={(e) => handleFiles(e.target.files)}
175
+ />
176
+ <div className="flex flex-col items-center gap-3">
177
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-[var(--lokvis-surface-muted)]">
178
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-[var(--lokvis-fg-subtle)]">
179
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
180
+ <polyline points="17 8 12 3 7 8" />
181
+ <line x1="12" y1="3" x2="12" y2="15" />
182
+ </svg>
183
+ </div>
184
+ <div>
185
+ <p className="text-sm font-medium text-[var(--lokvis-fg)]">
186
+ Drop file here or click to upload
187
+ </p>
188
+ <p className="mt-1 text-xs text-[var(--lokvis-fg-subtle)]">
189
+ Accepted: {accept || 'any file'}
190
+ </p>
191
+ </div>
192
+ </div>
193
+ </label>
194
+ )}
195
+
196
+ {/* Processing area */}
197
+ {inputBlob && (
198
+ <div className="grid gap-6 md:grid-cols-2">
199
+ {/* Left: Parameters */}
200
+ <div className="space-y-4">
201
+ <Card>
202
+ <div className="mb-3 flex items-center justify-between">
203
+ <h3 className="text-sm font-semibold text-[var(--lokvis-fg)]">
204
+ Parameters
205
+ </h3>
206
+ <span className="rounded bg-[var(--lokvis-surface-muted)] px-2 py-0.5 font-mono text-xs text-[var(--lokvis-primary)]">
207
+ {capability.name}
208
+ </span>
209
+ </div>
210
+ <ParamForm
211
+ capability={capability}
212
+ values={params}
213
+ onChange={setParams}
214
+ />
215
+ </Card>
216
+
217
+ {/* Actions */}
218
+ <div className="flex gap-3">
219
+ <Button
220
+ variant="primary"
221
+ size="md"
222
+ onClick={handleProcess}
223
+ loading={processing}
224
+ disabled={!inputBlob}
225
+ >
226
+ {processing ? 'Processing...' : 'Process'}
227
+ </Button>
228
+ <Button
229
+ variant="secondary"
230
+ size="md"
231
+ onClick={() => {
232
+ setInputBlob(null);
233
+ if (inputUrl) URL.revokeObjectURL(inputUrl);
234
+ setInputUrl(null);
235
+ setOutputBlob(null);
236
+ if (outputUrl) {
237
+ URL.revokeObjectURL(outputUrl);
238
+ setOutputUrl(null);
239
+ }
240
+ }}
241
+ >
242
+ Change file
243
+ </Button>
244
+ {onOpenInWorkspace && (
245
+ <Button
246
+ variant="ghost"
247
+ size="md"
248
+ onClick={() => onOpenInWorkspace({ inputBlob, outputBlob, params })}
249
+ >
250
+ Open in Workspace →
251
+ </Button>
252
+ )}
253
+ </div>
254
+
255
+ {/* Progress */}
256
+ {processing && (
257
+ <div className="space-y-2">
258
+ <div className="flex items-center justify-between text-xs text-[var(--lokvis-fg-muted)]">
259
+ <span>Processing...</span>
260
+ <span className="tabular-nums">{Math.round(progress)}%</span>
261
+ </div>
262
+ <div className="h-1.5 overflow-hidden rounded-full bg-[var(--lokvis-border)]">
263
+ <div
264
+ className="h-full rounded-full bg-[var(--lokvis-primary)] transition-all duration-200"
265
+ style={{ width: `${progress}%` }}
266
+ />
267
+ </div>
268
+ <button
269
+ type="button"
270
+ onClick={handleCancel}
271
+ className="text-xs text-[var(--lokvis-danger)] hover:underline"
272
+ >
273
+ Cancel
274
+ </button>
275
+ </div>
276
+ )}
277
+
278
+ {/* Error */}
279
+ {error && (
280
+ <div className="rounded-lg border border-[var(--lokvis-danger)]/30 bg-[var(--lokvis-danger)]/10 px-4 py-3 text-sm text-[var(--lokvis-danger)]">
281
+ {error}
282
+ </div>
283
+ )}
284
+
285
+ {/* Download */}
286
+ {outputBlob && outputUrl && (
287
+ <Card>
288
+ <div className="flex items-center justify-between">
289
+ <div>
290
+ <p className="text-sm font-medium text-[var(--lokvis-fg)]">Ready to download</p>
291
+ <p className="mt-1 text-xs text-[var(--lokvis-fg-muted)]">
292
+ {(outputBlob.size / 1024).toFixed(1)} KB · {outputBlob.type.split('/')[1] ?? 'file'}
293
+ </p>
294
+ </div>
295
+ <a
296
+ href={outputUrl}
297
+ download={downloadName}
298
+ className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--lokvis-primary)] px-4 py-2 text-sm font-medium text-[var(--lokvis-primary-fg)] transition-colors hover:bg-[var(--lokvis-primary-hover)]"
299
+ >
300
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
301
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
302
+ <polyline points="7 10 12 15 17 10" />
303
+ <line x1="12" y1="15" x2="12" y2="3" />
304
+ </svg>
305
+ Download
306
+ </a>
307
+ </div>
308
+ </Card>
309
+ )}
310
+ </div>
311
+
312
+ {/* Right: Preview */}
313
+ <div className="space-y-4">
314
+ {/* Input preview */}
315
+ {inputUrl && (
316
+ <Card>
317
+ <h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
318
+ Input
319
+ </h4>
320
+ <img
321
+ src={inputUrl}
322
+ alt="Input preview"
323
+ className="max-h-80 w-full rounded-lg object-contain"
324
+ />
325
+ <p className="mt-2 text-xs text-[var(--lokvis-fg-muted)]">
326
+ {(inputBlob.size / 1024).toFixed(1)} KB · {inputBlob.type.split('/')[1] ?? 'file'}
327
+ </p>
328
+ </Card>
329
+ )}
330
+
331
+ {/* Output preview */}
332
+ {outputUrl && (
333
+ <Card>
334
+ <h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
335
+ Output
336
+ </h4>
337
+ <img
338
+ src={outputUrl}
339
+ alt="Output preview"
340
+ className="max-h-80 w-full rounded-lg object-contain"
341
+ />
342
+ <p className="mt-2 text-xs text-[var(--lokvis-fg-muted)]">
343
+ {(outputBlob!.size / 1024).toFixed(1)} KB · {outputBlob!.type.split('/')[1] ?? 'file'}
344
+ </p>
345
+ </Card>
346
+ )}
347
+ </div>
348
+ </div>
349
+ )}
350
+ </div>
351
+ );
352
+ }
@@ -11,77 +11,77 @@ import { Button } from '@lokvis/ui-core';
11
11
  import { useWorkspaceStore } from '../store/index.js';
12
12
 
13
13
  export interface ToolbarProps {
14
- title?: string;
15
- rightExtra?: React.ReactNode;
14
+ title?: string;
15
+ rightExtra?: React.ReactNode;
16
16
  }
17
17
 
18
18
  export function Toolbar({ title = 'Lokvis Workspace', rightExtra }: ToolbarProps) {
19
- const running = useWorkspaceStore((s) => s.running);
20
- const statusMessage = useWorkspaceStore((s) => s.statusMessage);
21
- const error = useWorkspaceStore((s) => s.error);
22
- const nodes = useWorkspaceStore((s) => s.nodes);
23
- const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
24
- const run = useWorkspaceStore((s) => s.run);
25
- const clearWorkflow = useWorkspaceStore((s) => s.clearWorkflow);
26
- const setError = useWorkspaceStore((s) => s.setError);
19
+ const running = useWorkspaceStore((s) => s.running);
20
+ const statusMessage = useWorkspaceStore((s) => s.statusMessage);
21
+ const error = useWorkspaceStore((s) => s.error);
22
+ const nodes = useWorkspaceStore((s) => s.nodes);
23
+ const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
24
+ const run = useWorkspaceStore((s) => s.run);
25
+ const clearWorkflow = useWorkspaceStore((s) => s.clearWorkflow);
26
+ const setError = useWorkspaceStore((s) => s.setError);
27
27
 
28
- const canRun = !running && nodes.length > 0 && !!selectedAssetId;
28
+ const canRun = !running && nodes.length > 0 && !!selectedAssetId;
29
29
 
30
- async function handleRun() {
31
- try {
32
- await run();
33
- } catch (err) {
34
- setError(err instanceof Error ? err.message : String(err));
35
- }
36
- }
30
+ async function handleRun() {
31
+ try {
32
+ await run();
33
+ } catch (err) {
34
+ setError(err instanceof Error ? err.message : String(err));
35
+ }
36
+ }
37
37
 
38
- return (
39
- <header className="flex h-12 shrink-0 items-center justify-between border-b border-zinc-200 bg-white px-4 dark:border-zinc-800 dark:bg-zinc-900">
40
- {/* Left: Brand */}
41
- <div className="flex items-center gap-2.5 min-w-0">
42
- <span
43
- className="shrink-0 text-base font-bold leading-none"
44
- style={{
45
- background: 'linear-gradient(135deg, #6366f1, #a855f7)',
46
- WebkitBackgroundClip: 'text',
47
- WebkitTextFillColor: 'transparent',
48
- }}
49
- >
50
-
51
- </span>
52
- <span className="truncate text-sm font-semibold tracking-tight">{title}</span>
53
- </div>
38
+ return (
39
+ <header className="flex h-[var(--lokvis-toolbar-h)] shrink-0 items-center justify-between border-b border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-4">
40
+ {/* Left: Brand */}
41
+ <div className="flex items-center gap-2.5 min-w-0">
42
+ <span
43
+ className="shrink-0 text-base font-bold leading-none"
44
+ style={{
45
+ background: 'linear-gradient(135deg, var(--lokvis-primary), var(--lokvis-info))',
46
+ WebkitBackgroundClip: 'text',
47
+ WebkitTextFillColor: 'transparent',
48
+ }}
49
+ >
50
+
51
+ </span>
52
+ <span className="truncate text-sm font-semibold tracking-tight">{title}</span>
53
+ </div>
54
54
 
55
- {/* Center: Status */}
56
- <div className="flex flex-1 items-center justify-center px-4 min-w-0">
57
- <span className={`truncate text-xs ${error ? 'text-red-500' : 'text-zinc-400'}`}>
58
- {statusMessage}
59
- </span>
60
- </div>
55
+ {/* Center: Status */}
56
+ <div className="flex flex-1 items-center justify-center px-4 min-w-0">
57
+ <span className={`truncate text-xs ${error ? 'text-[var(--lokvis-danger)]' : 'text-[var(--lokvis-fg-subtle)]'}`}>
58
+ {statusMessage}
59
+ </span>
60
+ </div>
61
61
 
62
- {/* Right: Actions */}
63
- <div className="flex items-center gap-1.5 shrink-0">
64
- {rightExtra}
65
- {nodes.length > 0 && (
66
- <Button
67
- variant="ghost"
68
- size="sm"
69
- onClick={clearWorkflow}
70
- disabled={running}
71
- >
72
- Clear
73
- </Button>
74
- )}
75
- <Button
76
- variant="primary"
77
- size="sm"
78
- onClick={handleRun}
79
- loading={running}
80
- disabled={!canRun}
81
- >
82
- Run
83
- </Button>
84
- </div>
85
- </header>
86
- );
62
+ {/* Right: Actions */}
63
+ <div className="flex items-center gap-1.5 shrink-0">
64
+ {rightExtra}
65
+ {nodes.length > 0 && (
66
+ <Button
67
+ variant="ghost"
68
+ size="sm"
69
+ onClick={clearWorkflow}
70
+ disabled={running}
71
+ >
72
+ Clear
73
+ </Button>
74
+ )}
75
+ <Button
76
+ variant="primary"
77
+ size="sm"
78
+ onClick={handleRun}
79
+ loading={running}
80
+ disabled={!canRun}
81
+ >
82
+ Run
83
+ </Button>
84
+ </div>
85
+ </header>
86
+ );
87
87
  }