@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.
- package/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +14 -14
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +8 -8
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +6 -6
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +7 -7
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +9 -9
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +6 -6
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +6 -6
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +7 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +13 -13
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +11 -11
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +25 -25
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +3 -3
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +22 -22
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +30 -30
- package/dist/components/Workspace.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/components/AssetPanel.tsx +263 -263
- package/src/components/Canvas.tsx +174 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +163 -163
- package/src/components/DownloadPanel.tsx +203 -203
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +170 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +136 -136
- package/src/components/ParamForm.tsx +136 -136
- package/src/components/PipelineBar.tsx +98 -98
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +148 -148
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +321 -321
- package/src/components/WorkflowTemplates.tsx +77 -77
- package/src/components/Workspace.tsx +223 -223
- package/src/css-modules.d.ts +3 -0
- package/src/index.ts +4 -0
|
@@ -11,283 +11,283 @@ import { Icon } from '@lokvis/ui-core';
|
|
|
11
11
|
import { useWorkspaceStore } from '../store/index.js';
|
|
12
12
|
|
|
13
13
|
export interface AssetPanelProps {
|
|
14
|
-
|
|
14
|
+
className?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** 类型筛选 chip 显示名 */
|
|
18
18
|
const TYPE_LABEL: Record<AssetType, string> = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
image: 'Image',
|
|
20
|
+
video: 'Video',
|
|
21
|
+
audio: 'Audio',
|
|
22
|
+
pdf: 'PDF',
|
|
23
|
+
text: 'Text',
|
|
24
|
+
data: 'Data',
|
|
25
|
+
unknown: 'Other',
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export function AssetPanel({ className = '' }: AssetPanelProps) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const assets = useWorkspaceStore((s) => s.assets);
|
|
30
|
+
const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
|
|
31
|
+
const thumbnails = useWorkspaceStore((s) => s.thumbnails);
|
|
32
|
+
const importFiles = useWorkspaceStore((s) => s.importFiles);
|
|
33
|
+
const selectAsset = useWorkspaceStore((s) => s.selectAsset);
|
|
34
|
+
const removeAsset = useWorkspaceStore((s) => s.removeAsset);
|
|
35
|
+
const setThumbnail = useWorkspaceStore((s) => s.setThumbnail);
|
|
36
|
+
const runtime = useWorkspaceStore((s) => s.runtime);
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const [dragging, setDragging] = React.useState(false);
|
|
39
|
+
// W6.5 筛选:类型 + 关键词(纯 UI 状态,不入 store)
|
|
40
|
+
const [filterType, setFilterType] = React.useState<'all' | AssetType>('all');
|
|
41
|
+
const [searchQuery, setSearchQuery] = React.useState('');
|
|
42
|
+
// B2 修复:跟踪 in-flight exportAsset,避免同一 asset 跨 effect 重跑并发发起
|
|
43
|
+
const inflightRef = React.useRef<Set<string>>(new Set());
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
async function handleFiles(files: FileList | File[] | null) {
|
|
46
|
+
if (!files) return;
|
|
47
|
+
const arr = Array.from(files);
|
|
48
|
+
if (arr.length > 0) await importFiles(arr);
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
51
|
+
// 自动生成缩略图
|
|
52
|
+
// B2 修复:
|
|
53
|
+
// - cancelled flag:effect 重跑/卸载时,await 完成的 exportAsset 不再创建
|
|
54
|
+
// ObjectURL 也不 setThumbnail,避免覆盖更新值与孤儿 URL
|
|
55
|
+
// - inflightRef:同一 asset 在上一轮 effect 还在 await 时,新一轮 effect
|
|
56
|
+
// 不再重复发起(批量导入 assets 频繁变化场景)
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
if (!runtime) return;
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
const inflight = inflightRef.current;
|
|
61
|
+
for (const asset of assets) {
|
|
62
|
+
if (thumbnails[asset.id]) continue;
|
|
63
|
+
if (asset.type !== 'image') continue;
|
|
64
|
+
if (inflight.has(asset.id)) continue;
|
|
65
|
+
inflight.add(asset.id);
|
|
66
|
+
(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const blob = await runtime.exportAsset(asset.id);
|
|
69
|
+
// effect 已过期或资产已删除:不创建 ObjectURL(零泄漏)
|
|
70
|
+
if (cancelled) return;
|
|
71
|
+
const url = URL.createObjectURL(blob);
|
|
72
|
+
if (cancelled) {
|
|
73
|
+
// 双重检查:await 与 createObjectURL 之间可能被取消
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setThumbnail(asset.id, url);
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore export 失败
|
|
80
|
+
} finally {
|
|
81
|
+
inflight.delete(asset.id);
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
}
|
|
85
|
+
return () => {
|
|
86
|
+
cancelled = true;
|
|
87
|
+
};
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
+
}, [assets, runtime]);
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
// 资产中实际存在的类型(仅展示有意义的 chip,避免空类型噪音)
|
|
92
|
+
const availableTypes = React.useMemo(() => {
|
|
93
|
+
const set = new Set<AssetType>();
|
|
94
|
+
for (const a of assets) set.add(a.type);
|
|
95
|
+
return Array.from(set);
|
|
96
|
+
}, [assets]);
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
98
|
+
// 筛选后的资产列表
|
|
99
|
+
const filteredAssets = React.useMemo(() => {
|
|
100
|
+
let result = assets;
|
|
101
|
+
if (filterType !== 'all') {
|
|
102
|
+
result = result.filter((a) => a.type === filterType);
|
|
103
|
+
}
|
|
104
|
+
const q = searchQuery.trim().toLowerCase();
|
|
105
|
+
if (q) {
|
|
106
|
+
result = result.filter(
|
|
107
|
+
(a) =>
|
|
108
|
+
a.metadata.format.toLowerCase().includes(q) ||
|
|
109
|
+
a.metadata.mimeType.toLowerCase().includes(q)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}, [assets, filterType, searchQuery]);
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
return (
|
|
116
|
+
<aside
|
|
117
|
+
className={`flex w-56 shrink-0 flex-col border-r border-[var(--lokvis-border)] ${className}`}
|
|
118
|
+
onDragOver={(e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
setDragging(true);
|
|
121
|
+
}}
|
|
122
|
+
onDragLeave={() => setDragging(false)}
|
|
123
|
+
onDrop={(e) => {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
setDragging(false);
|
|
126
|
+
void handleFiles(e.dataTransfer.files);
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{/* Header */}
|
|
130
|
+
<div className="flex items-center justify-between px-3 h-10 shrink-0 border-b border-[var(--lokvis-border)]">
|
|
131
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
132
|
+
Assets
|
|
133
|
+
</span>
|
|
134
|
+
<span className="text-[11px] tabular-nums text-[var(--lokvis-fg-subtle)]">
|
|
135
|
+
{filterType === 'all' && !searchQuery
|
|
136
|
+
? assets.length
|
|
137
|
+
: `${filteredAssets.length}/${assets.length}`}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
141
|
+
{/* Upload zone */}
|
|
142
|
+
<label
|
|
143
|
+
className={`m-2 block cursor-pointer rounded-lg border border-dashed px-2 py-3 text-center transition-all ${
|
|
144
|
+
dragging
|
|
145
|
+
? 'border-[var(--lokvis-primary)] bg-[var(--lokvis-primary)]/10 text-[var(--lokvis-primary)]'
|
|
146
|
+
: 'border-[var(--lokvis-border)] text-[var(--lokvis-fg-subtle)] hover:border-[var(--lokvis-primary)]/50 hover:bg-[var(--lokvis-primary)]/10/50 hover:text-[var(--lokvis-primary)]'
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
<input
|
|
150
|
+
type="file"
|
|
151
|
+
multiple
|
|
152
|
+
className="hidden"
|
|
153
|
+
onChange={(e) => handleFiles(e.target.files)}
|
|
154
|
+
/>
|
|
155
|
+
<Icon size={16} className="mx-auto">
|
|
156
|
+
<path d="M12 4.5v15m7.5-7.5h-15" />
|
|
157
|
+
</Icon>
|
|
158
|
+
<span className="mt-1 block text-[10px] font-medium">
|
|
159
|
+
{dragging ? 'Drop here' : 'Add files'}
|
|
160
|
+
</span>
|
|
161
|
+
</label>
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
163
|
+
{/* W6.5 筛选栏:仅当有资产时显示 */}
|
|
164
|
+
{assets.length > 0 && (
|
|
165
|
+
<div className="px-2 pb-2 space-y-1.5">
|
|
166
|
+
{/* 类型 chips:m10 radiogroup 语义,屏幕阅读器识别为单选组 */}
|
|
167
|
+
<div
|
|
168
|
+
role="radiogroup"
|
|
169
|
+
aria-label="Filter by type"
|
|
170
|
+
className="flex flex-wrap gap-1"
|
|
171
|
+
>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
role="radio"
|
|
175
|
+
aria-checked={filterType === 'all'}
|
|
176
|
+
onClick={() => setFilterType('all')}
|
|
177
|
+
className={`rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
|
178
|
+
filterType === 'all'
|
|
179
|
+
? 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)]'
|
|
180
|
+
: 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-border)]'
|
|
181
|
+
}`}
|
|
182
|
+
>
|
|
183
|
+
All
|
|
184
|
+
</button>
|
|
185
|
+
{availableTypes.map((t) => (
|
|
186
|
+
<button
|
|
187
|
+
key={t}
|
|
188
|
+
type="button"
|
|
189
|
+
role="radio"
|
|
190
|
+
aria-checked={filterType === t}
|
|
191
|
+
onClick={() => setFilterType(t)}
|
|
192
|
+
className={`rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
|
193
|
+
filterType === t
|
|
194
|
+
? 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)]'
|
|
195
|
+
: 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-border)]'
|
|
196
|
+
}`}
|
|
197
|
+
>
|
|
198
|
+
{TYPE_LABEL[t]}
|
|
199
|
+
</button>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
{/* 搜索框:m4 文案改为与实际过滤行为一致(按 format/mimeType) */}
|
|
203
|
+
<div className="relative">
|
|
204
|
+
<Icon
|
|
205
|
+
size={11}
|
|
206
|
+
className="absolute left-1.5 top-1/2 -translate-y-1/2 text-[var(--lokvis-fg-subtle)] pointer-events-none"
|
|
207
|
+
>
|
|
208
|
+
<circle cx="10" cy="10" r="6" />
|
|
209
|
+
<path d="m20 20-5-5" />
|
|
210
|
+
</Icon>
|
|
211
|
+
<input
|
|
212
|
+
type="text"
|
|
213
|
+
value={searchQuery}
|
|
214
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
215
|
+
placeholder="Filter by type..."
|
|
216
|
+
aria-label="Filter assets by format"
|
|
217
|
+
className="w-full rounded bg-[var(--lokvis-surface-muted)] py-1 pl-6 pr-1.5 text-[10px] text-[var(--lokvis-fg-muted)] placeholder:text-[var(--lokvis-fg-subtle)] focus:outline-none focus:ring-1 focus:ring-[var(--lokvis-primary)]/50"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
223
|
+
{/* Asset list */}
|
|
224
|
+
<div className="flex-1 overflow-y-auto px-2 pb-2">
|
|
225
|
+
{assets.length === 0 ? (
|
|
226
|
+
<p className="px-1 py-4 text-center text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
227
|
+
No assets imported
|
|
228
|
+
</p>
|
|
229
|
+
) : filteredAssets.length === 0 ? (
|
|
230
|
+
<p className="px-1 py-4 text-center text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
231
|
+
No assets match filter
|
|
232
|
+
</p>
|
|
233
|
+
) : (
|
|
234
|
+
<ul className="space-y-1">
|
|
235
|
+
{filteredAssets.map((asset) => {
|
|
236
|
+
const selected = asset.id === selectedAssetId;
|
|
237
|
+
const thumb = thumbnails[asset.id];
|
|
238
|
+
return (
|
|
239
|
+
<li
|
|
240
|
+
key={asset.id}
|
|
241
|
+
// M3: 键盘可达 —— tabIndex + role + onKeyDown(Enter/Space 选中)
|
|
242
|
+
tabIndex={0}
|
|
243
|
+
role="button"
|
|
244
|
+
aria-pressed={selected}
|
|
245
|
+
aria-label={`Select asset ${asset.metadata.format}`}
|
|
246
|
+
onClick={() => selectAsset(asset.id)}
|
|
247
|
+
onKeyDown={(e) => {
|
|
248
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
selectAsset(asset.id);
|
|
251
|
+
}
|
|
252
|
+
}}
|
|
253
|
+
className={`group flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-all focus:outline-none focus:ring-2 focus:ring-[var(--lokvis-primary)]/50 ${
|
|
254
|
+
selected
|
|
255
|
+
? 'bg-[var(--lokvis-primary)]/10 ring-1 ring-[var(--lokvis-primary)]/40'
|
|
256
|
+
: 'hover:bg-[var(--lokvis-surface)]'
|
|
257
|
+
}`}
|
|
258
|
+
>
|
|
259
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded bg-[var(--lokvis-surface-muted)]">
|
|
260
|
+
{thumb ? (
|
|
261
|
+
<img src={thumb} alt="" className="h-full w-full object-cover" />
|
|
262
|
+
) : (
|
|
263
|
+
<span className="text-[10px] font-medium text-[var(--lokvis-fg-subtle)]">{asset.metadata.format.slice(0, 3).toUpperCase()}</span>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
<div className="min-w-0 flex-1">
|
|
267
|
+
<div className="truncate text-[11px] font-medium">
|
|
268
|
+
{asset.metadata.format}
|
|
269
|
+
</div>
|
|
270
|
+
<div className="text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
271
|
+
{(asset.metadata.size / 1024).toFixed(1)} KB
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={(e) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
void removeAsset(asset.id);
|
|
279
|
+
}}
|
|
280
|
+
className="shrink-0 rounded p-0.5 text-[var(--lokvis-fg-subtle)] opacity-0 transition-all hover:bg-[var(--lokvis-danger)]/10 hover:text-[var(--lokvis-danger)] focus:opacity-100 group-hover:opacity-100"
|
|
281
|
+
aria-label={`Remove asset ${asset.metadata.format}`}
|
|
282
|
+
>
|
|
283
|
+
<Icon size={12}><path d="M6 18L18 6M6 6l12 12" /></Icon>
|
|
284
|
+
</button>
|
|
285
|
+
</li>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
288
|
+
</ul>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
</aside>
|
|
292
|
+
);
|
|
293
293
|
}
|