@lokvis/ui-react 0.2.2 → 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.
- package/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +10 -54
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +4 -4
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +4 -3
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +5 -4
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +1 -8
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts +40 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +52 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ExifPanel.js +1 -1
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +10 -3
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +10 -7
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +8 -5
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +17 -26
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +3 -12
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/StatusDot.d.ts +12 -0
- package/dist/components/StatusDot.d.ts.map +1 -0
- package/dist/components/StatusDot.js +15 -0
- package/dist/components/StatusDot.js.map +1 -0
- package/dist/components/ThemeToggle.js +1 -1
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/ToolRunner.d.ts +53 -0
- package/dist/components/ToolRunner.d.ts.map +1 -0
- package/dist/components/ToolRunner.js +142 -0
- package/dist/components/ToolRunner.js.map +1 -0
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +31 -40
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +15 -5
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +23 -1
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +60 -13
- package/dist/components/Workspace.js.map +1 -1
- package/dist/hooks/useCustomPresets.d.ts +98 -0
- package/dist/hooks/useCustomPresets.d.ts.map +1 -0
- package/dist/hooks/useCustomPresets.js +210 -0
- package/dist/hooks/useCustomPresets.js.map +1 -0
- package/dist/hooks/useLokvis.d.ts +19 -1
- package/dist/hooks/useLokvis.d.ts.map +1 -1
- package/dist/hooks/useLokvis.js +30 -9
- package/dist/hooks/useLokvis.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts +8 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +101 -54
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/capabilities-slice.d.ts +1 -1
- package/dist/store/capabilities-slice.d.ts.map +1 -1
- package/dist/store/capabilities-slice.js +8 -1
- package/dist/store/capabilities-slice.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +22 -16
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/subscribe-utils.d.ts +45 -0
- package/dist/store/subscribe-utils.d.ts.map +1 -0
- package/dist/store/subscribe-utils.js +69 -0
- package/dist/store/subscribe-utils.js.map +1 -0
- package/dist/store/types.d.ts +19 -11
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +0 -10
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +19 -51
- package/dist/store/workflow-slice.js.map +1 -1
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +7 -6
- package/src/components/AssetPanel.tsx +18 -58
- package/src/components/Canvas.tsx +11 -7
- package/src/components/CompareSlider.tsx +13 -10
- package/src/components/DownloadPanel.tsx +1 -7
- package/src/components/ErrorBoundary.tsx +101 -0
- package/src/components/ExifPanel.tsx +1 -1
- package/src/components/GlobalDropzone.tsx +8 -3
- package/src/components/Inspector.tsx +24 -16
- package/src/components/ParamForm.tsx +12 -9
- package/src/components/PipelineBar.tsx +20 -27
- package/src/components/StatusBar.tsx +3 -10
- package/src/components/StatusDot.tsx +27 -0
- package/src/components/ThemeToggle.tsx +1 -1
- package/src/components/ToolRunner.tsx +352 -0
- package/src/components/Toolbar.tsx +2 -2
- package/src/components/WorkflowEditor.tsx +50 -43
- package/src/components/WorkflowTemplates.tsx +27 -4
- package/src/components/Workspace.tsx +97 -10
- package/src/hooks/useCustomPresets.ts +284 -0
- package/src/hooks/useLokvis.ts +43 -10
- package/src/index.ts +3 -0
- package/src/store/assets-slice.ts +106 -52
- package/src/store/capabilities-slice.ts +10 -2
- package/src/store/index.ts +1 -1
- package/src/store/runtime-slice.ts +20 -15
- package/src/store/subscribe-utils.ts +82 -0
- package/src/store/types.ts +19 -12
- package/src/store/workflow-slice.ts +19 -53
- package/src/utils.ts +41 -0
- package/dist/store.d.ts +0 -76
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -217
- package/dist/store.js.map +0 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorBoundary - React 错误边界(D6)
|
|
3
|
+
*
|
|
4
|
+
* 捕获子组件树渲染期间的运行时异常,避免整个 Workspace 白屏。
|
|
5
|
+
* 必须是 class 组件(React 错误边界 API 仅支持 class)。
|
|
6
|
+
*
|
|
7
|
+
* 设计:
|
|
8
|
+
* - 默认 fallback 使用 lokvis token,与 Workspace 错误态视觉一致
|
|
9
|
+
* - 可选 fallback 渲染 prop(消费方自定义 UI)
|
|
10
|
+
* - 可选 onError 回调(上报等);默认 console.error,不引入 sentry 等
|
|
11
|
+
* app 层依赖,保持 ui-react 包架构纯净(不反向依赖消费方)
|
|
12
|
+
* - reset() 清空 error 状态,尝试重新渲染子树
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <ErrorBoundary>
|
|
17
|
+
* <Workspace plugins={[...]} />
|
|
18
|
+
* </ErrorBoundary>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
|
|
24
|
+
export interface ErrorBoundaryProps {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
/** 自定义 fallback;不传则用默认 UI */
|
|
27
|
+
fallback?: (error: Error, reset: () => void) => React.ReactNode;
|
|
28
|
+
/** 错误回调(上报等),不传则 console.error */
|
|
29
|
+
onError?: (error: Error, info: React.ErrorInfo) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface State {
|
|
33
|
+
error: Error | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, State> {
|
|
37
|
+
state: State = { error: null };
|
|
38
|
+
|
|
39
|
+
static getDerivedStateFromError(error: Error): State {
|
|
40
|
+
return { error };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
44
|
+
if (this.props.onError) {
|
|
45
|
+
this.props.onError(error, info);
|
|
46
|
+
} else {
|
|
47
|
+
console.error('[lokvis ErrorBoundary]', error, info.componentStack);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reset = () => {
|
|
52
|
+
this.setState({ error: null });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
render() {
|
|
56
|
+
if (this.state.error) {
|
|
57
|
+
if (this.props.fallback) {
|
|
58
|
+
return this.props.fallback(this.state.error, this.reset);
|
|
59
|
+
}
|
|
60
|
+
return <DefaultFallback error={this.state.error} onReset={this.reset} />;
|
|
61
|
+
}
|
|
62
|
+
return this.props.children;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function DefaultFallback({ error, onReset }: { error: Error; onReset: () => void }) {
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex h-full w-full items-center justify-center p-6" role="alert">
|
|
69
|
+
<div className="max-w-md rounded-xl border border-[var(--lokvis-danger)]/30 bg-[var(--lokvis-surface)] p-6 text-center shadow-[var(--lokvis-elevation-2)]">
|
|
70
|
+
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--lokvis-danger)]/15">
|
|
71
|
+
<svg
|
|
72
|
+
width="20"
|
|
73
|
+
height="20"
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
fill="none"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth="2"
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
className="text-[var(--lokvis-danger)]"
|
|
81
|
+
>
|
|
82
|
+
<circle cx="12" cy="12" r="10" />
|
|
83
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
84
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
85
|
+
</svg>
|
|
86
|
+
</div>
|
|
87
|
+
<p className="font-semibold text-[var(--lokvis-fg)]">Something went wrong</p>
|
|
88
|
+
<p className="mt-1.5 break-words text-sm text-[var(--lokvis-fg-muted)]">
|
|
89
|
+
{error.message || 'An unexpected error occurred while rendering this panel.'}
|
|
90
|
+
</p>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={onReset}
|
|
94
|
+
className="mt-4 inline-flex items-center justify-center 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)]"
|
|
95
|
+
>
|
|
96
|
+
Try again
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -121,7 +121,7 @@ export function ExifPanel({ className = '' }: ExifPanelProps) {
|
|
|
121
121
|
<button
|
|
122
122
|
type="button"
|
|
123
123
|
onClick={() => setOpen(!open)}
|
|
124
|
-
className="flex w-full items-center justify-between px-3 h-
|
|
124
|
+
className="flex w-full items-center justify-between px-3 h-[var(--lokvis-panel-header-h)] text-left transition-colors hover:bg-[var(--lokvis-surface)]"
|
|
125
125
|
aria-expanded={open}
|
|
126
126
|
>
|
|
127
127
|
<div className="flex items-center gap-2">
|
|
@@ -76,6 +76,8 @@ export function GlobalDropzone({
|
|
|
76
76
|
// dragCounter:用计数器而非布尔,避免子元素 dragenter/dragleave 触发抖动
|
|
77
77
|
const [dragCounter, setDragCounter] = React.useState(0);
|
|
78
78
|
const [rejectedFiles, setRejectedFiles] = React.useState<string[]>([]);
|
|
79
|
+
// W21.6: 持有 rejectedFiles 自动清除定时器,unmount 时 cleanup
|
|
80
|
+
const rejectTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
79
81
|
|
|
80
82
|
React.useEffect(() => {
|
|
81
83
|
// 阻止浏览器默认行为(打开文件 / 下载),否则 drop 不会触发
|
|
@@ -134,8 +136,9 @@ export function GlobalDropzone({
|
|
|
134
136
|
}
|
|
135
137
|
if (rejected.length > 0) {
|
|
136
138
|
setRejectedFiles(rejected);
|
|
137
|
-
// 5
|
|
138
|
-
|
|
139
|
+
// W21.6: 5 秒后自动清空拒绝提示,clear 旧 timer 避免重叠
|
|
140
|
+
if (rejectTimerRef.current) clearTimeout(rejectTimerRef.current);
|
|
141
|
+
rejectTimerRef.current = setTimeout(() => setRejectedFiles([]), 5000);
|
|
139
142
|
}
|
|
140
143
|
if (accepted.length > 0) {
|
|
141
144
|
void importFiles(accepted);
|
|
@@ -154,6 +157,8 @@ export function GlobalDropzone({
|
|
|
154
157
|
window.removeEventListener('dragover', onDragOver);
|
|
155
158
|
window.removeEventListener('dragleave', onDragLeave);
|
|
156
159
|
window.removeEventListener('drop', onDrop);
|
|
160
|
+
// W21.6: 清理 rejectedFiles 自动清除定时器
|
|
161
|
+
if (rejectTimerRef.current) clearTimeout(rejectTimerRef.current);
|
|
157
162
|
};
|
|
158
163
|
}, [accept, importFiles, setStatus, setError]);
|
|
159
164
|
|
|
@@ -173,7 +178,7 @@ export function GlobalDropzone({
|
|
|
173
178
|
|
|
174
179
|
{/* 内容 */}
|
|
175
180
|
<div
|
|
176
|
-
className={`relative w-full max-w-md rounded-2xl border-2 border-dashed p-8 text-center bg-[var(--lokvis-surface)] shadow-
|
|
181
|
+
className={`relative w-full max-w-md rounded-2xl border-2 border-dashed p-8 text-center bg-[var(--lokvis-surface)] shadow-[var(--lokvis-elevation-overlay)] ${
|
|
177
182
|
hasRejections ? 'border-[var(--lokvis-danger)]' : 'border-[var(--lokvis-primary)]'
|
|
178
183
|
}`}
|
|
179
184
|
>
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as React from 'react';
|
|
9
|
-
import { Icon } from '@lokvis/ui-core';
|
|
9
|
+
import { Icon, Input } from '@lokvis/ui-core';
|
|
10
10
|
import { useWorkspaceStore } from '../store/index.js';
|
|
11
|
+
import { filterCapabilities } from '../utils.js';
|
|
11
12
|
import { ParamForm } from './ParamForm.js';
|
|
12
13
|
import { ExifPanel } from './ExifPanel.js';
|
|
13
14
|
|
|
@@ -20,6 +21,7 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
20
21
|
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
21
22
|
const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
|
|
22
23
|
const capabilityMap = useWorkspaceStore((s) => s.capabilityMap);
|
|
24
|
+
const stubCapabilities = useWorkspaceStore((s) => s.stubCapabilities);
|
|
23
25
|
const addNode = useWorkspaceStore((s) => s.addNode);
|
|
24
26
|
const updateNodeParams = useWorkspaceStore((s) => s.updateNodeParams);
|
|
25
27
|
|
|
@@ -27,11 +29,7 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
27
29
|
const [filter, setFilter] = React.useState('');
|
|
28
30
|
const [configureOpen, setConfigureOpen] = React.useState(true);
|
|
29
31
|
|
|
30
|
-
const filtered = capabilities
|
|
31
|
-
(c) =>
|
|
32
|
-
c.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
33
|
-
c.description.toLowerCase().includes(filter.toLowerCase())
|
|
34
|
-
);
|
|
32
|
+
const filtered = filterCapabilities(capabilities, filter);
|
|
35
33
|
|
|
36
34
|
// 按域分组
|
|
37
35
|
const grouped = React.useMemo(() => {
|
|
@@ -59,7 +57,9 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
59
57
|
<button
|
|
60
58
|
type="button"
|
|
61
59
|
onClick={() => setConfigureOpen(!configureOpen)}
|
|
62
|
-
|
|
60
|
+
aria-expanded={configureOpen}
|
|
61
|
+
aria-controls={`configure-panel-${selectedNode.id}`}
|
|
62
|
+
className="flex w-full items-center justify-between px-3 h-[var(--lokvis-panel-header-h)] text-left transition-colors hover:bg-[var(--lokvis-surface)]"
|
|
63
63
|
>
|
|
64
64
|
<div className="flex items-center gap-2">
|
|
65
65
|
<Icon size={12} className={`text-[var(--lokvis-fg-subtle)] transition-transform ${configureOpen ? 'rotate-90' : ''}`}><path d="m9 5 7 7-7 7" /></Icon>
|
|
@@ -70,7 +70,7 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
70
70
|
</span>
|
|
71
71
|
</button>
|
|
72
72
|
{configureOpen && (
|
|
73
|
-
<div className="px-3 pb-3 max-h-60 overflow-y-auto">
|
|
73
|
+
<div id={`configure-panel-${selectedNode.id}`} className="px-3 pb-3 max-h-60 overflow-y-auto">
|
|
74
74
|
<ParamForm
|
|
75
75
|
capability={capabilityMap[selectedNode.capability]!}
|
|
76
76
|
values={selectedNode.params}
|
|
@@ -83,7 +83,7 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
83
83
|
|
|
84
84
|
{/* Capabilities section */}
|
|
85
85
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
86
|
-
<div className="flex items-center justify-between px-3 h-
|
|
86
|
+
<div className="flex items-center justify-between px-3 h-[var(--lokvis-panel-header-h)] shrink-0 border-b border-[var(--lokvis-border)]">
|
|
87
87
|
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
88
88
|
Capabilities
|
|
89
89
|
</span>
|
|
@@ -92,17 +92,16 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
92
92
|
|
|
93
93
|
{/* Search */}
|
|
94
94
|
<div className="px-2 py-2 border-b border-[var(--lokvis-border)]">
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
<input
|
|
98
|
-
type="text"
|
|
95
|
+
<Input
|
|
96
|
+
size="sm"
|
|
99
97
|
placeholder="Search capabilities..."
|
|
100
98
|
value={filter}
|
|
101
99
|
onChange={(e) => setFilter(e.target.value)}
|
|
102
|
-
|
|
100
|
+
leadingIcon={
|
|
101
|
+
<Icon size={12}><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></Icon>
|
|
102
|
+
}
|
|
103
103
|
/>
|
|
104
104
|
</div>
|
|
105
|
-
</div>
|
|
106
105
|
|
|
107
106
|
{/* Capability list */}
|
|
108
107
|
<div className="flex-1 overflow-y-auto p-2">
|
|
@@ -123,11 +122,14 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
123
122
|
<ul className="space-y-0.5">
|
|
124
123
|
{caps.map((cap) => {
|
|
125
124
|
const isInPipeline = nodes.some((n) => n.capability === cap.name);
|
|
125
|
+
// A7: stub-only 能力显示 "Coming Soon" 标记(无可用引擎)
|
|
126
|
+
const isStubOnly = stubCapabilities.has(cap.name);
|
|
126
127
|
return (
|
|
127
128
|
<li key={cap.name}>
|
|
128
129
|
<button
|
|
129
130
|
type="button"
|
|
130
131
|
onClick={() => addNode(cap.name)}
|
|
132
|
+
title={isStubOnly ? 'Coming soon — no engine installed yet' : undefined}
|
|
131
133
|
className={`group w-full rounded-md px-2 py-1.5 text-left transition-all ${
|
|
132
134
|
isInPipeline
|
|
133
135
|
? 'bg-[var(--lokvis-primary)]/5 ring-1 ring-[var(--lokvis-primary)]/40'
|
|
@@ -135,7 +137,12 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
135
137
|
}`}
|
|
136
138
|
>
|
|
137
139
|
<div className="flex items-center justify-between gap-2">
|
|
138
|
-
<span className=
|
|
140
|
+
<span className={`truncate font-mono text-[11px] font-medium ${isStubOnly ? 'text-[var(--lokvis-fg-muted)]' : ''}`}>{cap.name}</span>
|
|
141
|
+
{isStubOnly ? (
|
|
142
|
+
<span className="shrink-0 rounded px-1 py-0.5 text-[9px] font-medium uppercase bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]">
|
|
143
|
+
Soon
|
|
144
|
+
</span>
|
|
145
|
+
) : (
|
|
139
146
|
<span className={`shrink-0 rounded px-1 py-0.5 text-[9px] font-medium uppercase ${
|
|
140
147
|
cap.performance === 'fast'
|
|
141
148
|
? 'bg-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
|
|
@@ -145,6 +152,7 @@ export function Inspector({ className = '' }: InspectorProps) {
|
|
|
145
152
|
}`}>
|
|
146
153
|
{cap.performance}
|
|
147
154
|
</span>
|
|
155
|
+
)}
|
|
148
156
|
</div>
|
|
149
157
|
<p className="mt-0.5 truncate text-[10px] text-[var(--lokvis-fg-muted)]">{cap.description}</p>
|
|
150
158
|
</button>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Capability, CapabilityParam } from '@lokvis/schema';
|
|
9
|
+
import { Input } from '@lokvis/ui-core';
|
|
9
10
|
|
|
10
11
|
export interface ParamFormProps {
|
|
11
12
|
capability: Capability;
|
|
@@ -40,8 +41,10 @@ export function ParamForm({ capability, values, onChange }: ParamFormProps) {
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// 仅用于 enum 的原生 <select>(Select 组件 API 不同,不在此迁移);
|
|
45
|
+
// 文本/数字输入已统一改用 <Input size="sm"> 组件,消除重复样式。
|
|
46
|
+
const selectBase =
|
|
47
|
+
'w-full rounded-md border border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-2.5 py-1.5 text-[11px] transition-colors hover:border-[var(--lokvis-border-strong)] focus:border-[var(--lokvis-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--lokvis-primary)]/30 disabled:cursor-not-allowed disabled:opacity-50';
|
|
45
48
|
|
|
46
49
|
function ParamField({
|
|
47
50
|
param,
|
|
@@ -79,7 +82,7 @@ function ParamField({
|
|
|
79
82
|
<select
|
|
80
83
|
value={(value as string) ?? (param.default as string) ?? ''}
|
|
81
84
|
onChange={(e) => onChange(e.target.value)}
|
|
82
|
-
className={
|
|
85
|
+
className={selectBase}
|
|
83
86
|
>
|
|
84
87
|
{param.values.map((v) => (
|
|
85
88
|
<option key={v} value={v}>{v}</option>
|
|
@@ -102,11 +105,11 @@ function ParamField({
|
|
|
102
105
|
onChange={(e) => onChange(e.target.value)}
|
|
103
106
|
className="h-7 w-7 shrink-0 cursor-pointer rounded border border-[var(--lokvis-border)]"
|
|
104
107
|
/>
|
|
105
|
-
<
|
|
108
|
+
<Input
|
|
106
109
|
type="text"
|
|
110
|
+
size="sm"
|
|
107
111
|
value={(value as string) ?? (param.default as string) ?? '#ffffff'}
|
|
108
112
|
onChange={(e) => onChange(e.target.value)}
|
|
109
|
-
className={inputBase}
|
|
110
113
|
/>
|
|
111
114
|
</div>
|
|
112
115
|
</div>
|
|
@@ -129,13 +132,13 @@ function ParamField({
|
|
|
129
132
|
</span>
|
|
130
133
|
)}
|
|
131
134
|
</div>
|
|
132
|
-
<
|
|
135
|
+
<Input
|
|
133
136
|
type="number"
|
|
137
|
+
size="sm"
|
|
134
138
|
value={value === undefined ? (param.default as number | undefined) ?? '' : (value as number)}
|
|
135
139
|
min={param.min}
|
|
136
140
|
max={param.max}
|
|
137
141
|
onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
|
|
138
|
-
className={inputBase}
|
|
139
142
|
/>
|
|
140
143
|
</div>
|
|
141
144
|
);
|
|
@@ -151,12 +154,12 @@ function ParamField({
|
|
|
151
154
|
<span className="ml-1 font-normal text-[var(--lokvis-fg-subtle)]">{param.description}</span>
|
|
152
155
|
)}
|
|
153
156
|
</label>
|
|
154
|
-
<
|
|
157
|
+
<Input
|
|
155
158
|
type="text"
|
|
159
|
+
size="sm"
|
|
156
160
|
value={(value as string) ?? (param.default as string) ?? ''}
|
|
157
161
|
onChange={(e) => onChange(e.target.value)}
|
|
158
162
|
placeholder={param.type === 'file' ? 'data URL or path' : ''}
|
|
159
|
-
className={inputBase}
|
|
160
163
|
/>
|
|
161
164
|
</div>
|
|
162
165
|
);
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as React from 'react';
|
|
9
|
-
import { Icon } from '@lokvis/ui-core';
|
|
9
|
+
import { FOCUS_RING, Icon } from '@lokvis/ui-core';
|
|
10
10
|
import { useWorkspaceStore } from '../store/index.js';
|
|
11
|
+
import { StatusDot } from './StatusDot.js';
|
|
11
12
|
|
|
12
13
|
export interface PipelineBarProps {
|
|
13
14
|
className?: string;
|
|
@@ -47,10 +48,21 @@ export function PipelineBar({ className = '' }: PipelineBarProps) {
|
|
|
47
48
|
const selected = node.id === selectedNodeId;
|
|
48
49
|
return (
|
|
49
50
|
<React.Fragment key={node.id}>
|
|
51
|
+
{/* D7: remove button 作为 node button 的兄弟而非子元素(HTML 规范禁止 button 嵌套) */}
|
|
52
|
+
<div className="group flex shrink-0 items-center gap-0.5">
|
|
50
53
|
<button
|
|
51
54
|
type="button"
|
|
52
55
|
onClick={() => selectNode(node.id)}
|
|
53
|
-
|
|
56
|
+
onKeyDown={(e) => {
|
|
57
|
+
// 键盘可达:Delete/Backspace 删除节点(与 WorkflowEditor 一致)
|
|
58
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
removeNode(node.id);
|
|
61
|
+
}
|
|
62
|
+
}}
|
|
63
|
+
aria-label={`节点 ${node.capability},位置 ${i + 1},Delete 删除`}
|
|
64
|
+
aria-pressed={selected}
|
|
65
|
+
className={`flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1 text-[11px] font-medium transition-all ${FOCUS_RING} ${
|
|
54
66
|
selected
|
|
55
67
|
? 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)] ring-1 ring-[var(--lokvis-primary)]/50'
|
|
56
68
|
: node.status === 'running'
|
|
@@ -64,18 +76,16 @@ export function PipelineBar({ className = '' }: PipelineBarProps) {
|
|
|
64
76
|
>
|
|
65
77
|
<span className="font-mono">{node.capability}</span>
|
|
66
78
|
<StatusDot status={node.status} />
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
removeNode(node.id)
|
|
71
|
-
|
|
72
|
-
className="ml-0.5 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-[var(--lokvis-danger)]/15 hover:text-[var(--lokvis-danger)]"
|
|
73
|
-
role="button"
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() => removeNode(node.id)}
|
|
83
|
+
className="rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--lokvis-danger)] hover:bg-[var(--lokvis-danger)]/15 hover:text-[var(--lokvis-danger)]"
|
|
74
84
|
aria-label="Remove step"
|
|
75
85
|
>
|
|
76
86
|
<Icon size={10} strokeWidth={3}><path d="M6 18L18 6M6 6l12 12" /></Icon>
|
|
77
|
-
</span>
|
|
78
87
|
</button>
|
|
88
|
+
</div>
|
|
79
89
|
|
|
80
90
|
{/* Arrow between nodes */}
|
|
81
91
|
{i < nodes.length - 1 && (
|
|
@@ -107,20 +117,3 @@ export function PipelineBar({ className = '' }: PipelineBarProps) {
|
|
|
107
117
|
</div>
|
|
108
118
|
);
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
function StatusDot({ status }: { status: string }) {
|
|
112
|
-
const color =
|
|
113
|
-
status === 'running'
|
|
114
|
-
? 'bg-[var(--lokvis-warning)]'
|
|
115
|
-
: status === 'success'
|
|
116
|
-
? 'bg-[var(--lokvis-success)]'
|
|
117
|
-
: status === 'failed'
|
|
118
|
-
? 'bg-[var(--lokvis-danger)]'
|
|
119
|
-
: status === 'pending'
|
|
120
|
-
? 'bg-[var(--lokvis-fg-subtle)]'
|
|
121
|
-
: 'bg-[var(--lokvis-fg-subtle)]';
|
|
122
|
-
|
|
123
|
-
const animate = status === 'running' ? 'animate-pulse' : '';
|
|
124
|
-
|
|
125
|
-
return <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${color} ${animate}`} />;
|
|
126
|
-
}
|
|
@@ -12,19 +12,12 @@
|
|
|
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
18
|
className?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
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`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
/** useOnlineStatus - 监听 navigator.onLine + online/offline 事件(W9.6) */
|
|
29
22
|
function useOnlineStatus(): boolean {
|
|
30
23
|
const [online, setOnline] = React.useState<boolean>(() =>
|
|
@@ -68,7 +61,7 @@ export function StatusBar({ className = '' }: StatusBarProps) {
|
|
|
68
61
|
|
|
69
62
|
return (
|
|
70
63
|
<footer
|
|
71
|
-
className={`flex h-
|
|
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}`}
|
|
72
65
|
>
|
|
73
66
|
{/* Left: Status — m5 加 aria-live,屏幕阅读器播报状态变化 */}
|
|
74
67
|
<div
|
|
@@ -116,7 +109,7 @@ export function StatusBar({ className = '' }: StatusBarProps) {
|
|
|
116
109
|
<button
|
|
117
110
|
type="button"
|
|
118
111
|
onClick={() => setError(null)}
|
|
119
|
-
className="shrink-0 text-[10px] text-[var(--lokvis-danger)] underline decoration-
|
|
112
|
+
className="shrink-0 text-[10px] text-[var(--lokvis-danger)] underline decoration-[var(--lokvis-danger)]/40 hover:text-[var(--lokvis-danger)]"
|
|
120
113
|
>
|
|
121
114
|
dismiss
|
|
122
115
|
</button>
|
|
@@ -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
|
+
}
|
|
@@ -85,7 +85,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
|
|
|
85
85
|
{menuOpen && (
|
|
86
86
|
<div
|
|
87
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-
|
|
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
89
|
>
|
|
90
90
|
{(['light', 'dark', null] as Array<ThemeMode | null>).map((mode) => {
|
|
91
91
|
const isActive = theme === mode;
|