@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
@@ -6,158 +6,166 @@
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
 
14
15
  export interface InspectorProps {
15
- className?: string;
16
+ className?: string;
16
17
  }
17
18
 
18
19
  export function Inspector({ className = '' }: InspectorProps) {
19
- const capabilities = useWorkspaceStore((s) => s.capabilities);
20
- const nodes = useWorkspaceStore((s) => s.nodes);
21
- const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
22
- const capabilityMap = useWorkspaceStore((s) => s.capabilityMap);
23
- const addNode = useWorkspaceStore((s) => s.addNode);
24
- const updateNodeParams = useWorkspaceStore((s) => s.updateNodeParams);
20
+ const capabilities = useWorkspaceStore((s) => s.capabilities);
21
+ const nodes = useWorkspaceStore((s) => s.nodes);
22
+ const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
23
+ const capabilityMap = useWorkspaceStore((s) => s.capabilityMap);
24
+ const stubCapabilities = useWorkspaceStore((s) => s.stubCapabilities);
25
+ const addNode = useWorkspaceStore((s) => s.addNode);
26
+ const updateNodeParams = useWorkspaceStore((s) => s.updateNodeParams);
25
27
 
26
- const selectedNode = nodes.find((n) => n.id === selectedNodeId);
27
- const [filter, setFilter] = React.useState('');
28
- const [configureOpen, setConfigureOpen] = React.useState(true);
28
+ const selectedNode = nodes.find((n) => n.id === selectedNodeId);
29
+ const [filter, setFilter] = React.useState('');
30
+ const [configureOpen, setConfigureOpen] = React.useState(true);
29
31
 
30
- const filtered = capabilities.filter(
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
- // 按域分组
37
- const grouped = React.useMemo(() => {
38
- const map = new Map<string, typeof filtered>();
39
- for (const cap of filtered) {
40
- const domain = cap.name.split('.')[0] ?? 'other';
41
- if (!map.has(domain)) map.set(domain, []);
42
- map.get(domain)!.push(cap);
43
- }
44
- return map;
45
- }, [filtered]);
34
+ // 按域分组
35
+ const grouped = React.useMemo(() => {
36
+ const map = new Map<string, typeof filtered>();
37
+ for (const cap of filtered) {
38
+ const domain = cap.name.split('.')[0] ?? 'other';
39
+ if (!map.has(domain)) map.set(domain, []);
40
+ map.get(domain)!.push(cap);
41
+ }
42
+ return map;
43
+ }, [filtered]);
46
44
 
47
- const showConfigure = selectedNode && capabilityMap[selectedNode.capability];
45
+ const showConfigure = selectedNode && capabilityMap[selectedNode.capability];
48
46
 
49
- return (
50
- <aside
51
- className={`flex w-72 shrink-0 flex-col border-l border-zinc-200 dark:border-zinc-800 ${className}`}
52
- >
53
- {/* EXIF panel (selected image asset only, hides automatically for non-image) */}
54
- <ExifPanel />
47
+ return (
48
+ <aside
49
+ className={`flex w-72 shrink-0 flex-col border-l border-[var(--lokvis-border)] ${className}`}
50
+ >
51
+ {/* EXIF panel (selected image asset only, hides automatically for non-image) */}
52
+ <ExifPanel />
55
53
 
56
- {/* Configure section (when a node is selected) */}
57
- {showConfigure && (
58
- <div className="shrink-0 border-b border-zinc-200 dark:border-zinc-800">
59
- <button
60
- type="button"
61
- onClick={() => setConfigureOpen(!configureOpen)}
62
- className="flex w-full items-center justify-between px-3 h-10 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
63
- >
64
- <div className="flex items-center gap-2">
65
- <Icon size={12} className={`text-zinc-400 transition-transform ${configureOpen ? 'rotate-90' : ''}`}><path d="m9 5 7 7-7 7" /></Icon>
66
- <span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400">Configure</span>
67
- </div>
68
- <span className="font-mono text-[10px] text-indigo-500 bg-indigo-50 dark:bg-indigo-950/30 px-1.5 py-0.5 rounded">
69
- {selectedNode.capability}
70
- </span>
71
- </button>
72
- {configureOpen && (
73
- <div className="px-3 pb-3 max-h-60 overflow-y-auto">
74
- <ParamForm
75
- capability={capabilityMap[selectedNode.capability]!}
76
- values={selectedNode.params}
77
- onChange={(params) => updateNodeParams(selectedNode.id, params)}
78
- />
79
- </div>
80
- )}
81
- </div>
82
- )}
54
+ {/* Configure section (when a node is selected) */}
55
+ {showConfigure && (
56
+ <div className="shrink-0 border-b border-[var(--lokvis-border)]">
57
+ <button
58
+ type="button"
59
+ onClick={() => setConfigureOpen(!configureOpen)}
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
+ >
64
+ <div className="flex items-center gap-2">
65
+ <Icon size={12} className={`text-[var(--lokvis-fg-subtle)] transition-transform ${configureOpen ? 'rotate-90' : ''}`}><path d="m9 5 7 7-7 7" /></Icon>
66
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">Configure</span>
67
+ </div>
68
+ <span className="font-mono text-[10px] text-[var(--lokvis-primary)] bg-[var(--lokvis-primary)]/10 px-1.5 py-0.5 rounded">
69
+ {selectedNode.capability}
70
+ </span>
71
+ </button>
72
+ {configureOpen && (
73
+ <div id={`configure-panel-${selectedNode.id}`} className="px-3 pb-3 max-h-60 overflow-y-auto">
74
+ <ParamForm
75
+ capability={capabilityMap[selectedNode.capability]!}
76
+ values={selectedNode.params}
77
+ onChange={(params) => updateNodeParams(selectedNode.id, params)}
78
+ />
79
+ </div>
80
+ )}
81
+ </div>
82
+ )}
83
83
 
84
- {/* Capabilities section */}
85
- <div className="flex flex-1 flex-col overflow-hidden">
86
- <div className="flex items-center justify-between px-3 h-10 shrink-0 border-b border-zinc-200 dark:border-zinc-800">
87
- <span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
88
- Capabilities
89
- </span>
90
- <span className="text-[11px] tabular-nums text-zinc-400">{filtered.length}</span>
91
- </div>
84
+ {/* Capabilities section */}
85
+ <div className="flex flex-1 flex-col overflow-hidden">
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
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
88
+ Capabilities
89
+ </span>
90
+ <span className="text-[11px] tabular-nums text-[var(--lokvis-fg-subtle)]">{filtered.length}</span>
91
+ </div>
92
92
 
93
- {/* Search */}
94
- <div className="px-2 py-2 border-b border-zinc-100 dark:border-zinc-800/50">
95
- <div className="relative">
96
- <Icon size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></Icon>
97
- <input
98
- type="text"
99
- placeholder="Search capabilities..."
100
- value={filter}
101
- onChange={(e) => setFilter(e.target.value)}
102
- className="w-full rounded-md border border-zinc-200 bg-white py-1 pl-7 pr-2 text-[11px] placeholder-zinc-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400 dark:border-zinc-700 dark:bg-zinc-900"
103
- />
104
- </div>
105
- </div>
93
+ {/* Search */}
94
+ <div className="px-2 py-2 border-b border-[var(--lokvis-border)]">
95
+ <Input
96
+ size="sm"
97
+ placeholder="Search capabilities..."
98
+ value={filter}
99
+ onChange={(e) => setFilter(e.target.value)}
100
+ leadingIcon={
101
+ <Icon size={12}><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></Icon>
102
+ }
103
+ />
104
+ </div>
106
105
 
107
- {/* Capability list */}
108
- <div className="flex-1 overflow-y-auto p-2">
109
- {capabilities.length === 0 ? (
110
- <div className="px-2 py-8 text-center">
111
- <p className="text-[11px] text-zinc-400">No capabilities loaded</p>
112
- <p className="mt-1 text-[10px] text-zinc-500">Load a plugin to get started</p>
113
- </div>
114
- ) : filtered.length === 0 ? (
115
- <p className="px-2 py-4 text-[11px] text-zinc-400">No matching capabilities</p>
116
- ) : (
117
- <div className="space-y-3">
118
- {Array.from(grouped.entries()).map(([domain, caps]) => (
119
- <div key={domain}>
120
- <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">
121
- {domain}
122
- </div>
123
- <ul className="space-y-0.5">
124
- {caps.map((cap) => {
125
- const isInPipeline = nodes.some((n) => n.capability === cap.name);
126
- return (
127
- <li key={cap.name}>
128
- <button
129
- type="button"
130
- onClick={() => addNode(cap.name)}
131
- className={`group w-full rounded-md px-2 py-1.5 text-left transition-all ${
132
- isInPipeline
133
- ? 'bg-indigo-50/50 ring-1 ring-indigo-200 dark:bg-indigo-950/20 dark:ring-indigo-800'
134
- : 'hover:bg-zinc-50 dark:hover:bg-zinc-800'
135
- }`}
136
- >
137
- <div className="flex items-center justify-between gap-2">
138
- <span className="truncate font-mono text-[11px] font-medium">{cap.name}</span>
139
- <span className={`shrink-0 rounded px-1 py-0.5 text-[9px] font-medium uppercase ${
140
- cap.performance === 'fast'
141
- ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
142
- : cap.performance === 'medium'
143
- ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
144
- : 'bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400'
145
- }`}>
146
- {cap.performance}
147
- </span>
148
- </div>
149
- <p className="mt-0.5 truncate text-[10px] text-zinc-500 dark:text-zinc-400">{cap.description}</p>
150
- </button>
151
- </li>
152
- );
153
- })}
154
- </ul>
155
- </div>
156
- ))}
157
- </div>
158
- )}
159
- </div>
160
- </div>
161
- </aside>
162
- );
106
+ {/* Capability list */}
107
+ <div className="flex-1 overflow-y-auto p-2">
108
+ {capabilities.length === 0 ? (
109
+ <div className="px-2 py-8 text-center">
110
+ <p className="text-[11px] text-[var(--lokvis-fg-subtle)]">No capabilities loaded</p>
111
+ <p className="mt-1 text-[10px] text-[var(--lokvis-fg-muted)]">Load a plugin to get started</p>
112
+ </div>
113
+ ) : filtered.length === 0 ? (
114
+ <p className="px-2 py-4 text-[11px] text-[var(--lokvis-fg-subtle)]">No matching capabilities</p>
115
+ ) : (
116
+ <div className="space-y-3">
117
+ {Array.from(grouped.entries()).map(([domain, caps]) => (
118
+ <div key={domain}>
119
+ <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
120
+ {domain}
121
+ </div>
122
+ <ul className="space-y-0.5">
123
+ {caps.map((cap) => {
124
+ const isInPipeline = nodes.some((n) => n.capability === cap.name);
125
+ // A7: stub-only 能力显示 "Coming Soon" 标记(无可用引擎)
126
+ const isStubOnly = stubCapabilities.has(cap.name);
127
+ return (
128
+ <li key={cap.name}>
129
+ <button
130
+ type="button"
131
+ onClick={() => addNode(cap.name)}
132
+ title={isStubOnly ? 'Coming soon no engine installed yet' : undefined}
133
+ className={`group w-full rounded-md px-2 py-1.5 text-left transition-all ${
134
+ isInPipeline
135
+ ? 'bg-[var(--lokvis-primary)]/5 ring-1 ring-[var(--lokvis-primary)]/40'
136
+ : 'hover:bg-[var(--lokvis-surface)]'
137
+ }`}
138
+ >
139
+ <div className="flex items-center justify-between gap-2">
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
+ ) : (
146
+ <span className={`shrink-0 rounded px-1 py-0.5 text-[9px] font-medium uppercase ${
147
+ cap.performance === 'fast'
148
+ ? 'bg-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
149
+ : cap.performance === 'medium'
150
+ ? 'bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]'
151
+ : 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)]'
152
+ }`}>
153
+ {cap.performance}
154
+ </span>
155
+ )}
156
+ </div>
157
+ <p className="mt-0.5 truncate text-[10px] text-[var(--lokvis-fg-muted)]">{cap.description}</p>
158
+ </button>
159
+ </li>
160
+ );
161
+ })}
162
+ </ul>
163
+ </div>
164
+ ))}
165
+ </div>
166
+ )}
167
+ </div>
168
+ </div>
169
+ </aside>
170
+ );
163
171
  }
@@ -6,158 +6,161 @@
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
- capability: Capability;
12
- values: Record<string, unknown>;
13
- onChange: (values: Record<string, unknown>) => void;
12
+ capability: Capability;
13
+ values: Record<string, unknown>;
14
+ onChange: (values: Record<string, unknown>) => void;
14
15
  }
15
16
 
16
17
  export function ParamForm({ capability, values, onChange }: ParamFormProps) {
17
- const set = (name: string, value: unknown) => {
18
- onChange({ ...values, [name]: value });
19
- };
18
+ const set = (name: string, value: unknown) => {
19
+ onChange({ ...values, [name]: value });
20
+ };
20
21
 
21
- if (capability.params.length === 0) {
22
- return (
23
- <p className="py-3 text-center text-[11px] text-zinc-400 italic">
24
- This capability has no configurable parameters.
25
- </p>
26
- );
27
- }
22
+ if (capability.params.length === 0) {
23
+ return (
24
+ <p className="py-3 text-center text-[11px] text-[var(--lokvis-fg-subtle)] italic">
25
+ This capability has no configurable parameters.
26
+ </p>
27
+ );
28
+ }
28
29
 
29
- return (
30
- <div className="space-y-2.5">
31
- {capability.params.map((p) => (
32
- <ParamField
33
- key={p.name}
34
- param={p}
35
- value={values[p.name]}
36
- onChange={(v) => set(p.name, v)}
37
- />
38
- ))}
39
- </div>
40
- );
30
+ return (
31
+ <div className="space-y-2.5">
32
+ {capability.params.map((p) => (
33
+ <ParamField
34
+ key={p.name}
35
+ param={p}
36
+ value={values[p.name]}
37
+ onChange={(v) => set(p.name, v)}
38
+ />
39
+ ))}
40
+ </div>
41
+ );
41
42
  }
42
43
 
43
- const inputBase =
44
- 'w-full rounded-md border border-zinc-200 bg-white px-2.5 py-1.5 text-[11px] transition-colors focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400 dark:border-zinc-700 dark:bg-zinc-900';
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
- param,
48
- value,
49
- onChange,
50
+ param,
51
+ value,
52
+ onChange,
50
53
  }: {
51
- param: CapabilityParam;
52
- value: unknown;
53
- onChange: (value: unknown) => void;
54
+ param: CapabilityParam;
55
+ value: unknown;
56
+ onChange: (value: unknown) => void;
54
57
  }) {
55
- if (param.type === 'boolean') {
56
- return (
57
- <label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50">
58
- <input
59
- type="checkbox"
60
- checked={Boolean(value ?? param.default)}
61
- onChange={(e) => onChange(e.target.checked)}
62
- className="h-3.5 w-3.5 rounded border-zinc-300 text-indigo-600 focus:ring-indigo-500"
63
- />
64
- <span className="flex-1 text-[11px] text-zinc-700 dark:text-zinc-300">{param.name}</span>
65
- {param.description && (
66
- <span className="shrink-0 text-[10px] text-zinc-400">{param.description}</span>
67
- )}
68
- </label>
69
- );
70
- }
58
+ if (param.type === 'boolean') {
59
+ return (
60
+ <label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-[var(--lokvis-surface)]">
61
+ <input
62
+ type="checkbox"
63
+ checked={Boolean(value ?? param.default)}
64
+ onChange={(e) => onChange(e.target.checked)}
65
+ className="h-3.5 w-3.5 rounded border-[var(--lokvis-border-strong)] text-[var(--lokvis-primary)] focus:ring-[var(--lokvis-primary)]"
66
+ />
67
+ <span className="flex-1 text-[11px] text-[var(--lokvis-fg-muted)]">{param.name}</span>
68
+ {param.description && (
69
+ <span className="shrink-0 text-[10px] text-[var(--lokvis-fg-subtle)]">{param.description}</span>
70
+ )}
71
+ </label>
72
+ );
73
+ }
71
74
 
72
- if (param.type === 'enum' && param.values) {
73
- return (
74
- <div>
75
- <label className="mb-1 block text-[11px] font-medium text-zinc-600 dark:text-zinc-300">
76
- {param.name}
77
- {param.required && <span className="ml-0.5 text-red-500">*</span>}
78
- </label>
79
- <select
80
- value={(value as string) ?? (param.default as string) ?? ''}
81
- onChange={(e) => onChange(e.target.value)}
82
- className={inputBase}
83
- >
84
- {param.values.map((v) => (
85
- <option key={v} value={v}>{v}</option>
86
- ))}
87
- </select>
88
- </div>
89
- );
90
- }
75
+ if (param.type === 'enum' && param.values) {
76
+ return (
77
+ <div>
78
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
79
+ {param.name}
80
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</span>}
81
+ </label>
82
+ <select
83
+ value={(value as string) ?? (param.default as string) ?? ''}
84
+ onChange={(e) => onChange(e.target.value)}
85
+ className={selectBase}
86
+ >
87
+ {param.values.map((v) => (
88
+ <option key={v} value={v}>{v}</option>
89
+ ))}
90
+ </select>
91
+ </div>
92
+ );
93
+ }
91
94
 
92
- if (param.type === 'color') {
93
- return (
94
- <div>
95
- <label className="mb-1 block text-[11px] font-medium text-zinc-600 dark:text-zinc-300">
96
- {param.name}
97
- </label>
98
- <div className="flex gap-2">
99
- <input
100
- type="color"
101
- value={(value as string) ?? (param.default as string) ?? '#ffffff'}
102
- onChange={(e) => onChange(e.target.value)}
103
- className="h-7 w-7 shrink-0 cursor-pointer rounded border border-zinc-200 dark:border-zinc-700"
104
- />
105
- <input
106
- type="text"
107
- value={(value as string) ?? (param.default as string) ?? '#ffffff'}
108
- onChange={(e) => onChange(e.target.value)}
109
- className={inputBase}
110
- />
111
- </div>
112
- </div>
113
- );
114
- }
95
+ if (param.type === 'color') {
96
+ return (
97
+ <div>
98
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
99
+ {param.name}
100
+ </label>
101
+ <div className="flex gap-2">
102
+ <input
103
+ type="color"
104
+ value={(value as string) ?? (param.default as string) ?? '#ffffff'}
105
+ onChange={(e) => onChange(e.target.value)}
106
+ className="h-7 w-7 shrink-0 cursor-pointer rounded border border-[var(--lokvis-border)]"
107
+ />
108
+ <Input
109
+ type="text"
110
+ size="sm"
111
+ value={(value as string) ?? (param.default as string) ?? '#ffffff'}
112
+ onChange={(e) => onChange(e.target.value)}
113
+ />
114
+ </div>
115
+ </div>
116
+ );
117
+ }
115
118
 
116
- if (param.type === 'number') {
117
- return (
118
- <div>
119
- <div className="mb-1 flex items-center justify-between">
120
- <label className="text-[11px] font-medium text-zinc-600 dark:text-zinc-300">
121
- {param.name}
122
- {param.required && <span className="ml-0.5 text-red-500">*</span>}
123
- </label>
124
- {(param.min !== undefined || param.max !== undefined) && (
125
- <span className="text-[10px] text-zinc-400">
126
- {param.min !== undefined ? param.min : '-'}
127
- {' — '}
128
- {param.max !== undefined ? param.max : '∞'}
129
- </span>
130
- )}
131
- </div>
132
- <input
133
- type="number"
134
- value={value === undefined ? (param.default as number | undefined) ?? '' : (value as number)}
135
- min={param.min}
136
- max={param.max}
137
- onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
138
- className={inputBase}
139
- />
140
- </div>
141
- );
142
- }
119
+ if (param.type === 'number') {
120
+ return (
121
+ <div>
122
+ <div className="mb-1 flex items-center justify-between">
123
+ <label className="text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
124
+ {param.name}
125
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</span>}
126
+ </label>
127
+ {(param.min !== undefined || param.max !== undefined) && (
128
+ <span className="text-[10px] text-[var(--lokvis-fg-subtle)]">
129
+ {param.min !== undefined ? param.min : '-'}
130
+ {' — '}
131
+ {param.max !== undefined ? param.max : '∞'}
132
+ </span>
133
+ )}
134
+ </div>
135
+ <Input
136
+ type="number"
137
+ size="sm"
138
+ value={value === undefined ? (param.default as number | undefined) ?? '' : (value as number)}
139
+ min={param.min}
140
+ max={param.max}
141
+ onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
142
+ />
143
+ </div>
144
+ );
145
+ }
143
146
 
144
- // string / file / array / object → text input
145
- return (
146
- <div>
147
- <label className="mb-1 block text-[11px] font-medium text-zinc-600 dark:text-zinc-300">
148
- {param.name}
149
- {param.required && <span className="ml-0.5 text-red-500">*</span>}
150
- {param.description && (
151
- <span className="ml-1 font-normal text-zinc-400">{param.description}</span>
152
- )}
153
- </label>
154
- <input
155
- type="text"
156
- value={(value as string) ?? (param.default as string) ?? ''}
157
- onChange={(e) => onChange(e.target.value)}
158
- placeholder={param.type === 'file' ? 'data URL or path' : ''}
159
- className={inputBase}
160
- />
161
- </div>
162
- );
147
+ // string / file / array / object → text input
148
+ return (
149
+ <div>
150
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
151
+ {param.name}
152
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</span>}
153
+ {param.description && (
154
+ <span className="ml-1 font-normal text-[var(--lokvis-fg-subtle)]">{param.description}</span>
155
+ )}
156
+ </label>
157
+ <Input
158
+ type="text"
159
+ size="sm"
160
+ value={(value as string) ?? (param.default as string) ?? ''}
161
+ onChange={(e) => onChange(e.target.value)}
162
+ placeholder={param.type === 'file' ? 'data URL or path' : ''}
163
+ />
164
+ </div>
165
+ );
163
166
  }