@lokvis/ui-react 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/components/AssetPanel.d.ts.map +1 -1
  2. package/dist/components/AssetPanel.js +14 -14
  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 +8 -8
  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 +6 -6
  13. package/dist/components/CompareSlider.d.ts.map +1 -1
  14. package/dist/components/CompareSlider.js +7 -7
  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 +9 -9
  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/ExifPanel.d.ts +1 -1
  25. package/dist/components/ExifPanel.d.ts.map +1 -1
  26. package/dist/components/ExifPanel.js +2 -2
  27. package/dist/components/ExifPanel.js.map +1 -1
  28. package/dist/components/GlobalDropzone.d.ts +5 -5
  29. package/dist/components/GlobalDropzone.d.ts.map +1 -1
  30. package/dist/components/GlobalDropzone.js +6 -6
  31. package/dist/components/GlobalDropzone.js.map +1 -1
  32. package/dist/components/HistoryPanel.d.ts +2 -2
  33. package/dist/components/HistoryPanel.d.ts.map +1 -1
  34. package/dist/components/HistoryPanel.js +23 -23
  35. package/dist/components/HistoryPanel.js.map +1 -1
  36. package/dist/components/Inspector.d.ts.map +1 -1
  37. package/dist/components/Inspector.js +6 -6
  38. package/dist/components/Inspector.js.map +1 -1
  39. package/dist/components/ParamForm.d.ts.map +1 -1
  40. package/dist/components/ParamForm.js +7 -7
  41. package/dist/components/ParamForm.js.map +1 -1
  42. package/dist/components/PipelineBar.d.ts.map +1 -1
  43. package/dist/components/PipelineBar.js +13 -13
  44. package/dist/components/PipelineBar.js.map +1 -1
  45. package/dist/components/ProgressBar.d.ts +4 -4
  46. package/dist/components/ProgressBar.d.ts.map +1 -1
  47. package/dist/components/ProgressBar.js +8 -8
  48. package/dist/components/ProgressBar.js.map +1 -1
  49. package/dist/components/StatusBar.d.ts +3 -3
  50. package/dist/components/StatusBar.d.ts.map +1 -1
  51. package/dist/components/StatusBar.js +11 -11
  52. package/dist/components/StatusBar.js.map +1 -1
  53. package/dist/components/ThemeToggle.d.ts +4 -4
  54. package/dist/components/ThemeToggle.d.ts.map +1 -1
  55. package/dist/components/ThemeToggle.js +7 -7
  56. package/dist/components/ThemeToggle.js.map +1 -1
  57. package/dist/components/Toolbar.d.ts.map +1 -1
  58. package/dist/components/Toolbar.js +2 -2
  59. package/dist/components/Toolbar.js.map +1 -1
  60. package/dist/components/WorkflowEditor.d.ts +6 -6
  61. package/dist/components/WorkflowEditor.d.ts.map +1 -1
  62. package/dist/components/WorkflowEditor.js +25 -25
  63. package/dist/components/WorkflowEditor.js.map +1 -1
  64. package/dist/components/WorkflowTemplates.d.ts +2 -2
  65. package/dist/components/WorkflowTemplates.d.ts.map +1 -1
  66. package/dist/components/WorkflowTemplates.js +3 -3
  67. package/dist/components/WorkflowTemplates.js.map +1 -1
  68. package/dist/components/Workspace.d.ts +22 -22
  69. package/dist/components/Workspace.d.ts.map +1 -1
  70. package/dist/components/Workspace.js +30 -30
  71. package/dist/components/Workspace.js.map +1 -1
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +3 -0
  75. package/dist/index.js.map +1 -1
  76. package/package.json +6 -6
  77. package/src/components/AssetPanel.tsx +263 -263
  78. package/src/components/Canvas.tsx +174 -174
  79. package/src/components/CommandPalette.tsx +242 -242
  80. package/src/components/CompareSlider.tsx +163 -163
  81. package/src/components/DownloadPanel.tsx +203 -203
  82. package/src/components/ErrorBanner.tsx +64 -64
  83. package/src/components/ExifPanel.tsx +154 -154
  84. package/src/components/GlobalDropzone.tsx +170 -170
  85. package/src/components/HistoryPanel.tsx +219 -219
  86. package/src/components/Inspector.tsx +136 -136
  87. package/src/components/ParamForm.tsx +136 -136
  88. package/src/components/PipelineBar.tsx +98 -98
  89. package/src/components/ProgressBar.tsx +78 -78
  90. package/src/components/StatusBar.tsx +148 -148
  91. package/src/components/ThemeToggle.tsx +98 -98
  92. package/src/components/Toolbar.tsx +65 -65
  93. package/src/components/WorkflowEditor.tsx +321 -321
  94. package/src/components/WorkflowTemplates.tsx +77 -77
  95. package/src/components/Workspace.tsx +223 -223
  96. package/src/css-modules.d.ts +3 -0
  97. package/src/index.ts +4 -0
@@ -12,152 +12,152 @@ import { ParamForm } from './ParamForm.js';
12
12
  import { ExifPanel } from './ExifPanel.js';
13
13
 
14
14
  export interface InspectorProps {
15
- className?: string;
15
+ className?: string;
16
16
  }
17
17
 
18
18
  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);
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);
25
25
 
26
- const selectedNode = nodes.find((n) => n.id === selectedNodeId);
27
- const [filter, setFilter] = React.useState('');
28
- const [configureOpen, setConfigureOpen] = React.useState(true);
26
+ const selectedNode = nodes.find((n) => n.id === selectedNodeId);
27
+ const [filter, setFilter] = React.useState('');
28
+ const [configureOpen, setConfigureOpen] = React.useState(true);
29
29
 
30
- const filtered = capabilities.filter(
31
- (c) =>
32
- c.name.toLowerCase().includes(filter.toLowerCase()) ||
33
- c.description.toLowerCase().includes(filter.toLowerCase())
34
- );
30
+ const filtered = capabilities.filter(
31
+ (c) =>
32
+ c.name.toLowerCase().includes(filter.toLowerCase()) ||
33
+ c.description.toLowerCase().includes(filter.toLowerCase())
34
+ );
35
35
 
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]);
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]);
46
46
 
47
- const showConfigure = selectedNode && capabilityMap[selectedNode.capability];
47
+ const showConfigure = selectedNode && capabilityMap[selectedNode.capability];
48
48
 
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 />
49
+ return (
50
+ <aside
51
+ className={`flex w-72 shrink-0 flex-col border-l border-[var(--lokvis-border)] ${className}`}
52
+ >
53
+ {/* EXIF panel (selected image asset only, hides automatically for non-image) */}
54
+ <ExifPanel />
55
55
 
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
- )}
56
+ {/* Configure section (when a node is selected) */}
57
+ {showConfigure && (
58
+ <div className="shrink-0 border-b border-[var(--lokvis-border)]">
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-[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 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-10 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
+ <div className="relative">
96
+ <Icon size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-[var(--lokvis-fg-subtle)]"><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-[var(--lokvis-border)] bg-[var(--lokvis-surface)] py-1 pl-7 pr-2 text-[11px] placeholder-zinc-400 focus:border-[var(--lokvis-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--lokvis-primary)]"
103
+ />
104
+ </div>
105
+ </div>
106
106
 
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
- );
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-[var(--lokvis-fg-subtle)]">No capabilities loaded</p>
112
+ <p className="mt-1 text-[10px] text-[var(--lokvis-fg-muted)]">Load a plugin to get started</p>
113
+ </div>
114
+ ) : filtered.length === 0 ? (
115
+ <p className="px-2 py-4 text-[11px] text-[var(--lokvis-fg-subtle)]">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-[var(--lokvis-fg-subtle)]">
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-[var(--lokvis-primary)]/5 ring-1 ring-[var(--lokvis-primary)]/40'
134
+ : 'hover:bg-[var(--lokvis-surface)]'
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-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
142
+ : cap.performance === 'medium'
143
+ ? 'bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]'
144
+ : 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)]'
145
+ }`}>
146
+ {cap.performance}
147
+ </span>
148
+ </div>
149
+ <p className="mt-0.5 truncate text-[10px] text-[var(--lokvis-fg-muted)]">{cap.description}</p>
150
+ </button>
151
+ </li>
152
+ );
153
+ })}
154
+ </ul>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+ </aside>
162
+ );
163
163
  }
@@ -8,156 +8,156 @@
8
8
  import type { Capability, CapabilityParam } from '@lokvis/schema';
9
9
 
10
10
  export interface ParamFormProps {
11
- capability: Capability;
12
- values: Record<string, unknown>;
13
- onChange: (values: Record<string, unknown>) => void;
11
+ capability: Capability;
12
+ values: Record<string, unknown>;
13
+ onChange: (values: Record<string, unknown>) => void;
14
14
  }
15
15
 
16
16
  export function ParamForm({ capability, values, onChange }: ParamFormProps) {
17
- const set = (name: string, value: unknown) => {
18
- onChange({ ...values, [name]: value });
19
- };
17
+ const set = (name: string, value: unknown) => {
18
+ onChange({ ...values, [name]: value });
19
+ };
20
20
 
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
- }
21
+ if (capability.params.length === 0) {
22
+ return (
23
+ <p className="py-3 text-center text-[11px] text-[var(--lokvis-fg-subtle)] italic">
24
+ This capability has no configurable parameters.
25
+ </p>
26
+ );
27
+ }
28
28
 
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
- );
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
+ );
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
+ 'w-full rounded-md border border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-2.5 py-1.5 text-[11px] transition-colors focus:border-[var(--lokvis-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--lokvis-primary)]';
45
45
 
46
46
  function ParamField({
47
- param,
48
- value,
49
- onChange,
47
+ param,
48
+ value,
49
+ onChange,
50
50
  }: {
51
- param: CapabilityParam;
52
- value: unknown;
53
- onChange: (value: unknown) => void;
51
+ param: CapabilityParam;
52
+ value: unknown;
53
+ onChange: (value: unknown) => void;
54
54
  }) {
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
- }
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-[var(--lokvis-surface)]">
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-[var(--lokvis-border-strong)] text-[var(--lokvis-primary)] focus:ring-[var(--lokvis-primary)]"
63
+ />
64
+ <span className="flex-1 text-[11px] text-[var(--lokvis-fg-muted)]">{param.name}</span>
65
+ {param.description && (
66
+ <span className="shrink-0 text-[10px] text-[var(--lokvis-fg-subtle)]">{param.description}</span>
67
+ )}
68
+ </label>
69
+ );
70
+ }
71
71
 
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
- }
72
+ if (param.type === 'enum' && param.values) {
73
+ return (
74
+ <div>
75
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
76
+ {param.name}
77
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</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
+ }
91
91
 
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
- }
92
+ if (param.type === 'color') {
93
+ return (
94
+ <div>
95
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
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-[var(--lokvis-border)]"
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
+ }
115
115
 
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
- }
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-[var(--lokvis-fg-muted)]">
121
+ {param.name}
122
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</span>}
123
+ </label>
124
+ {(param.min !== undefined || param.max !== undefined) && (
125
+ <span className="text-[10px] text-[var(--lokvis-fg-subtle)]">
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
+ }
143
143
 
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
- );
144
+ // string / file / array / object → text input
145
+ return (
146
+ <div>
147
+ <label className="mb-1 block text-[11px] font-medium text-[var(--lokvis-fg-muted)]">
148
+ {param.name}
149
+ {param.required && <span className="ml-0.5 text-[var(--lokvis-danger)]">*</span>}
150
+ {param.description && (
151
+ <span className="ml-1 font-normal text-[var(--lokvis-fg-subtle)]">{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
+ );
163
163
  }