@lokvis/ui-react 0.1.0 → 0.2.0

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 (160) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/AssetPanel.d.ts +2 -2
  3. package/dist/components/AssetPanel.d.ts.map +1 -1
  4. package/dist/components/AssetPanel.js +80 -7
  5. package/dist/components/AssetPanel.js.map +1 -1
  6. package/dist/components/Canvas.d.ts +9 -1
  7. package/dist/components/Canvas.d.ts.map +1 -1
  8. package/dist/components/Canvas.js +41 -3
  9. package/dist/components/Canvas.js.map +1 -1
  10. package/dist/components/CommandPalette.d.ts +48 -0
  11. package/dist/components/CommandPalette.d.ts.map +1 -0
  12. package/dist/components/CommandPalette.js +154 -0
  13. package/dist/components/CommandPalette.js.map +1 -0
  14. package/dist/components/CompareSlider.d.ts +25 -0
  15. package/dist/components/CompareSlider.d.ts.map +1 -0
  16. package/dist/components/CompareSlider.js +106 -0
  17. package/dist/components/CompareSlider.js.map +1 -0
  18. package/dist/components/DownloadPanel.d.ts +20 -0
  19. package/dist/components/DownloadPanel.d.ts.map +1 -0
  20. package/dist/components/DownloadPanel.js +169 -0
  21. package/dist/components/DownloadPanel.js.map +1 -0
  22. package/dist/components/ErrorBanner.d.ts +22 -0
  23. package/dist/components/ErrorBanner.d.ts.map +1 -0
  24. package/dist/components/ErrorBanner.js +47 -0
  25. package/dist/components/ErrorBanner.js.map +1 -0
  26. package/dist/components/ExifPanel.d.ts +26 -0
  27. package/dist/components/ExifPanel.d.ts.map +1 -0
  28. package/dist/components/ExifPanel.js +104 -0
  29. package/dist/components/ExifPanel.js.map +1 -0
  30. package/dist/components/GlobalDropzone.d.ts +37 -0
  31. package/dist/components/GlobalDropzone.d.ts.map +1 -0
  32. package/dist/components/GlobalDropzone.js +154 -0
  33. package/dist/components/GlobalDropzone.js.map +1 -0
  34. package/dist/components/HistoryPanel.d.ts +17 -7
  35. package/dist/components/HistoryPanel.d.ts.map +1 -1
  36. package/dist/components/HistoryPanel.js +74 -62
  37. package/dist/components/HistoryPanel.js.map +1 -1
  38. package/dist/components/Inspector.d.ts.map +1 -1
  39. package/dist/components/Inspector.js +3 -1
  40. package/dist/components/Inspector.js.map +1 -1
  41. package/dist/components/PipelineBar.d.ts.map +1 -1
  42. package/dist/components/PipelineBar.js +4 -3
  43. package/dist/components/PipelineBar.js.map +1 -1
  44. package/dist/components/ProgressBar.d.ts +18 -0
  45. package/dist/components/ProgressBar.d.ts.map +1 -0
  46. package/dist/components/ProgressBar.js +35 -0
  47. package/dist/components/ProgressBar.js.map +1 -0
  48. package/dist/components/StatusBar.d.ts +8 -2
  49. package/dist/components/StatusBar.d.ts.map +1 -1
  50. package/dist/components/StatusBar.js +59 -4
  51. package/dist/components/StatusBar.js.map +1 -1
  52. package/dist/components/ThemeToggle.d.ts +20 -0
  53. package/dist/components/ThemeToggle.d.ts.map +1 -0
  54. package/dist/components/ThemeToggle.js +60 -0
  55. package/dist/components/ThemeToggle.js.map +1 -0
  56. package/dist/components/WorkflowEditor.d.ts +24 -0
  57. package/dist/components/WorkflowEditor.d.ts.map +1 -0
  58. package/dist/components/WorkflowEditor.js +160 -0
  59. package/dist/components/WorkflowEditor.js.map +1 -0
  60. package/dist/components/WorkflowTemplates.d.ts +23 -0
  61. package/dist/components/WorkflowTemplates.d.ts.map +1 -0
  62. package/dist/components/WorkflowTemplates.js +32 -0
  63. package/dist/components/WorkflowTemplates.js.map +1 -0
  64. package/dist/components/Workspace.d.ts +48 -2
  65. package/dist/components/Workspace.d.ts.map +1 -1
  66. package/dist/components/Workspace.js +80 -5
  67. package/dist/components/Workspace.js.map +1 -1
  68. package/dist/data/workflow-templates.d.ts +42 -0
  69. package/dist/data/workflow-templates.d.ts.map +1 -0
  70. package/dist/data/workflow-templates.js +112 -0
  71. package/dist/data/workflow-templates.js.map +1 -0
  72. package/dist/hooks/useDebouncedRun.d.ts +49 -0
  73. package/dist/hooks/useDebouncedRun.d.ts.map +1 -0
  74. package/dist/hooks/useDebouncedRun.js +117 -0
  75. package/dist/hooks/useDebouncedRun.js.map +1 -0
  76. package/dist/hooks/useLokvis.js +1 -1
  77. package/dist/hooks/useMediaQuery.d.ts +27 -0
  78. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  79. package/dist/hooks/useMediaQuery.js +41 -0
  80. package/dist/hooks/useMediaQuery.js.map +1 -0
  81. package/dist/hooks/useShareLink.d.ts +58 -0
  82. package/dist/hooks/useShareLink.d.ts.map +1 -0
  83. package/dist/hooks/useShareLink.js +120 -0
  84. package/dist/hooks/useShareLink.js.map +1 -0
  85. package/dist/hooks/useTheme.d.ts +30 -0
  86. package/dist/hooks/useTheme.d.ts.map +1 -0
  87. package/dist/hooks/useTheme.js +105 -0
  88. package/dist/hooks/useTheme.js.map +1 -0
  89. package/dist/hooks/useWorkflowProgress.d.ts +28 -0
  90. package/dist/hooks/useWorkflowProgress.d.ts.map +1 -0
  91. package/dist/hooks/useWorkflowProgress.js +25 -0
  92. package/dist/hooks/useWorkflowProgress.js.map +1 -0
  93. package/dist/hooks/useWorkflows.d.ts +86 -0
  94. package/dist/hooks/useWorkflows.d.ts.map +1 -0
  95. package/dist/hooks/useWorkflows.js +247 -0
  96. package/dist/hooks/useWorkflows.js.map +1 -0
  97. package/dist/index.d.ts +26 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +26 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/store/assets-slice.d.ts.map +1 -1
  102. package/dist/store/assets-slice.js +19 -0
  103. package/dist/store/assets-slice.js.map +1 -1
  104. package/dist/store/history-slice.d.ts +18 -0
  105. package/dist/store/history-slice.d.ts.map +1 -0
  106. package/dist/store/history-slice.js +42 -0
  107. package/dist/store/history-slice.js.map +1 -0
  108. package/dist/store/index.d.ts +1 -0
  109. package/dist/store/index.d.ts.map +1 -1
  110. package/dist/store/index.js +3 -0
  111. package/dist/store/index.js.map +1 -1
  112. package/dist/store/runtime-slice.d.ts +10 -2
  113. package/dist/store/runtime-slice.d.ts.map +1 -1
  114. package/dist/store/runtime-slice.js +77 -28
  115. package/dist/store/runtime-slice.js.map +1 -1
  116. package/dist/store/types.d.ts +70 -1
  117. package/dist/store/types.d.ts.map +1 -1
  118. package/dist/store/types.js +10 -0
  119. package/dist/store/types.js.map +1 -1
  120. package/dist/store/workflow-slice.d.ts +1 -1
  121. package/dist/store/workflow-slice.d.ts.map +1 -1
  122. package/dist/store/workflow-slice.js +167 -20
  123. package/dist/store/workflow-slice.js.map +1 -1
  124. package/dist/store.d.ts +76 -0
  125. package/dist/store.d.ts.map +1 -0
  126. package/dist/store.js +217 -0
  127. package/dist/store.js.map +1 -0
  128. package/package.json +9 -6
  129. package/src/components/AssetPanel.tsx +154 -12
  130. package/src/components/Canvas.tsx +85 -7
  131. package/src/components/CommandPalette.tsx +300 -0
  132. package/src/components/CompareSlider.tsx +186 -0
  133. package/src/components/DownloadPanel.tsx +252 -0
  134. package/src/components/ErrorBanner.tsx +95 -0
  135. package/src/components/ExifPanel.tsx +187 -0
  136. package/src/components/GlobalDropzone.tsx +220 -0
  137. package/src/components/HistoryPanel.tsx +231 -104
  138. package/src/components/Inspector.tsx +7 -2
  139. package/src/components/PipelineBar.tsx +13 -12
  140. package/src/components/ProgressBar.tsx +104 -0
  141. package/src/components/StatusBar.tsx +124 -5
  142. package/src/components/ThemeToggle.tsx +122 -0
  143. package/src/components/WorkflowEditor.tsx +382 -0
  144. package/src/components/WorkflowTemplates.tsx +101 -0
  145. package/src/components/Workspace.tsx +207 -9
  146. package/src/data/workflow-templates.ts +137 -0
  147. package/src/hooks/useDebouncedRun.ts +151 -0
  148. package/src/hooks/useLokvis.ts +1 -1
  149. package/src/hooks/useMediaQuery.ts +53 -0
  150. package/src/hooks/useShareLink.ts +158 -0
  151. package/src/hooks/useTheme.ts +116 -0
  152. package/src/hooks/useWorkflowProgress.ts +41 -0
  153. package/src/hooks/useWorkflows.ts +326 -0
  154. package/src/index.ts +32 -0
  155. package/src/store/assets-slice.ts +19 -0
  156. package/src/store/history-slice.ts +65 -0
  157. package/src/store/index.ts +3 -0
  158. package/src/store/runtime-slice.ts +89 -32
  159. package/src/store/types.ts +71 -1
  160. package/src/store/workflow-slice.ts +178 -20
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/store.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Workspace UI 状态管理
3
+ *
4
+ * 使用 Zustand 管理工作台状态。Runtime 永远不知道 React,
5
+ * 但 UI 层负责把 Runtime 事件桥接到 store。
6
+ */
7
+ import { create } from 'zustand';
8
+ /** 生成节点 ID */
9
+ function genNodeId() {
10
+ return `node_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
11
+ }
12
+ export const useWorkspaceStore = create((set, get) => ({
13
+ runtime: null,
14
+ initializing: false,
15
+ initError: null,
16
+ assets: [],
17
+ selectedAssetId: null,
18
+ thumbnails: {},
19
+ capabilities: [],
20
+ capabilityMap: {},
21
+ nodes: [],
22
+ selectedNodeId: null,
23
+ running: false,
24
+ statusMessage: 'Idle',
25
+ error: null,
26
+ async init(runtime) {
27
+ set({ runtime, initializing: true, initError: null });
28
+ try {
29
+ await get().refreshAssets();
30
+ await get().refreshCapabilities();
31
+ set({ initializing: false, statusMessage: 'Ready' });
32
+ }
33
+ catch (err) {
34
+ set({
35
+ initializing: false,
36
+ initError: err instanceof Error ? err.message : String(err),
37
+ });
38
+ }
39
+ },
40
+ async refreshAssets() {
41
+ const { runtime } = get();
42
+ if (!runtime)
43
+ return;
44
+ const assets = await runtime.listAssets();
45
+ set({ assets });
46
+ },
47
+ async refreshCapabilities() {
48
+ const { runtime } = get();
49
+ if (!runtime)
50
+ return;
51
+ const capabilities = await runtime.capabilities();
52
+ const capabilityMap = {};
53
+ for (const cap of capabilities)
54
+ capabilityMap[cap.name] = cap;
55
+ set({ capabilities, capabilityMap });
56
+ },
57
+ async importFiles(files) {
58
+ const { runtime } = get();
59
+ if (!runtime)
60
+ return;
61
+ set({ statusMessage: `Importing ${files.length} file(s)...` });
62
+ for (const file of files) {
63
+ await runtime.importAsset({ kind: 'file', file });
64
+ }
65
+ await get().refreshAssets();
66
+ set({ statusMessage: `Imported ${files.length} file(s)` });
67
+ },
68
+ selectAsset(id) {
69
+ set({ selectedAssetId: id });
70
+ },
71
+ setThumbnail(id, url) {
72
+ set((state) => ({ thumbnails: { ...state.thumbnails, [id]: url } }));
73
+ },
74
+ async removeAsset(id) {
75
+ const { runtime } = get();
76
+ if (!runtime)
77
+ return;
78
+ await runtime.removeAsset(id);
79
+ await get().refreshAssets();
80
+ if (get().selectedAssetId === id)
81
+ set({ selectedAssetId: null });
82
+ },
83
+ addNode(capability) {
84
+ const node = {
85
+ id: genNodeId(),
86
+ capability,
87
+ params: {},
88
+ status: 'idle',
89
+ };
90
+ set((state) => ({
91
+ nodes: [...state.nodes, node],
92
+ selectedNodeId: node.id,
93
+ }));
94
+ },
95
+ updateNodeParams(id, params) {
96
+ set((state) => ({
97
+ nodes: state.nodes.map((n) => (n.id === id ? { ...n, params } : n)),
98
+ }));
99
+ },
100
+ removeNode(id) {
101
+ set((state) => ({
102
+ nodes: state.nodes.filter((n) => n.id !== id),
103
+ selectedNodeId: state.selectedNodeId === id ? null : state.selectedNodeId,
104
+ }));
105
+ },
106
+ selectNode(id) {
107
+ set({ selectedNodeId: id });
108
+ },
109
+ setNodeStatus(id, status, error, duration) {
110
+ set((state) => ({
111
+ nodes: state.nodes.map((n) => n.id === id ? { ...n, status, error, duration } : n),
112
+ }));
113
+ },
114
+ async run() {
115
+ const { runtime, nodes, selectedAssetId } = get();
116
+ if (!runtime)
117
+ throw new Error('Runtime not initialized');
118
+ if (nodes.length === 0)
119
+ throw new Error('Workflow is empty');
120
+ if (!selectedAssetId)
121
+ throw new Error('No asset selected');
122
+ set({ running: true, error: null, statusMessage: 'Running...' });
123
+ // 重置所有节点状态
124
+ set((state) => ({
125
+ nodes: state.nodes.map((n) => ({ ...n, status: 'pending', error: undefined })),
126
+ }));
127
+ try {
128
+ // 构建 Workflow
129
+ const workflow = buildLinearWorkflow(nodes);
130
+ const input = await runtime.getAsset(selectedAssetId);
131
+ set({ statusMessage: 'Running workflow...' });
132
+ const result = await runtime.run(workflow, [input]);
133
+ // 更新节点状态
134
+ if (result.status === 'completed') {
135
+ for (const node of nodes) {
136
+ get().setNodeStatus(node.id, 'success', undefined, undefined);
137
+ }
138
+ }
139
+ else if (result.status === 'failed') {
140
+ for (const node of nodes) {
141
+ if (get().nodes.find((n) => n.id === node.id)?.status === 'pending') {
142
+ get().setNodeStatus(node.id, 'failed', result.error);
143
+ }
144
+ }
145
+ }
146
+ // 加载输出资产
147
+ const outputs = [];
148
+ for (const id of result.outputs) {
149
+ try {
150
+ outputs.push(await runtime.getAsset(id));
151
+ }
152
+ catch {
153
+ // skip
154
+ }
155
+ }
156
+ set({
157
+ running: false,
158
+ statusMessage: `Workflow ${result.status} in ${result.duration}ms`,
159
+ });
160
+ if (result.status === 'failed') {
161
+ set({ error: result.error ?? 'Workflow failed' });
162
+ }
163
+ await get().refreshAssets();
164
+ return outputs;
165
+ }
166
+ catch (err) {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ set({
169
+ running: false,
170
+ error: message,
171
+ statusMessage: 'Failed',
172
+ });
173
+ throw err;
174
+ }
175
+ },
176
+ setStatus(message) {
177
+ set({ statusMessage: message });
178
+ },
179
+ setError(error) {
180
+ set({ error });
181
+ },
182
+ clearWorkflow() {
183
+ set({ nodes: [], selectedNodeId: null });
184
+ },
185
+ }));
186
+ /** 把工作台节点序列构建为线性 Workflow */
187
+ function buildLinearWorkflow(nodes) {
188
+ if (nodes.length === 0) {
189
+ throw new Error('Workflow is empty');
190
+ }
191
+ const workflowNodes = nodes.map((n) => ({
192
+ id: n.id,
193
+ type: 'transform',
194
+ capability: n.capability,
195
+ params: n.params,
196
+ }));
197
+ const edges = [];
198
+ for (let i = 0; i < workflowNodes.length - 1; i++) {
199
+ edges.push({ from: workflowNodes[i].id, to: workflowNodes[i + 1].id });
200
+ }
201
+ return {
202
+ id: `wf_${Date.now().toString(36)}`,
203
+ name: 'Workspace Workflow',
204
+ version: '1',
205
+ description: 'Workspace linear workflow',
206
+ author: { id: 'local', name: 'Local User' },
207
+ category: 'image',
208
+ tags: [],
209
+ nodes: workflowNodes,
210
+ edges,
211
+ inputs: { type: 'image', multiple: true },
212
+ outputs: { type: 'image' },
213
+ createdAt: Date.now(),
214
+ updatedAt: Date.now(),
215
+ };
216
+ }
217
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@lokvis/ui-react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lokvis Workspace UI - React components for Image/Video/PDF Workspace",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
7
10
  "main": "./dist/index.js",
8
11
  "module": "./dist/index.js",
9
12
  "types": "./dist/index.d.ts",
@@ -28,11 +31,11 @@
28
31
  },
29
32
  "dependencies": {
30
33
  "zustand": "^5.0.0",
31
- "@lokvis/schema": "0.1.0",
32
- "@lokvis/runtime": "0.1.0",
33
- "@lokvis/sdk": "0.1.0",
34
- "@lokvis/ui-core": "0.1.0",
35
- "@lokvis/capability": "0.1.0"
34
+ "@lokvis/schema": "0.2.0",
35
+ "@lokvis/runtime": "0.2.0",
36
+ "@lokvis/sdk": "0.2.0",
37
+ "@lokvis/ui-core": "0.2.0",
38
+ "@lokvis/capability": "0.2.0"
36
39
  },
37
40
  "devDependencies": {
38
41
  "@types/react": "^19.0.0",
@@ -1,17 +1,30 @@
1
1
  /**
2
2
  * AssetPanel - 左侧资产面板
3
3
  *
4
- * 紧凑布局:section header + 上传按钮 + 资产列表。
5
- * 支持拖拽导入、点击选择、缩略图预览。
4
+ * 紧凑布局:section header + 上传按钮 + 筛选 + 资产列表。
5
+ * 支持拖拽导入、点击选择、缩略图预览、按类型/关键词筛选、删除(W6.5)。
6
6
  */
7
7
 
8
8
  import * as React from 'react';
9
+ import type { AssetType } from '@lokvis/schema';
10
+ import { Icon } from '@lokvis/ui-core';
9
11
  import { useWorkspaceStore } from '../store/index.js';
10
12
 
11
13
  export interface AssetPanelProps {
12
14
  className?: string;
13
15
  }
14
16
 
17
+ /** 类型筛选 chip 显示名 */
18
+ const TYPE_LABEL: Record<AssetType, string> = {
19
+ image: 'Image',
20
+ video: 'Video',
21
+ audio: 'Audio',
22
+ pdf: 'PDF',
23
+ text: 'Text',
24
+ data: 'Data',
25
+ unknown: 'Other',
26
+ };
27
+
15
28
  export function AssetPanel({ className = '' }: AssetPanelProps) {
16
29
  const assets = useWorkspaceStore((s) => s.assets);
17
30
  const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
@@ -23,6 +36,11 @@ export function AssetPanel({ className = '' }: AssetPanelProps) {
23
36
  const runtime = useWorkspaceStore((s) => s.runtime);
24
37
 
25
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());
26
44
 
27
45
  async function handleFiles(files: FileList | File[] | null) {
28
46
  if (!files) return;
@@ -31,24 +49,69 @@ export function AssetPanel({ className = '' }: AssetPanelProps) {
31
49
  }
32
50
 
33
51
  // 自动生成缩略图
52
+ // B2 修复:
53
+ // - cancelled flag:effect 重跑/卸载时,await 完成的 exportAsset 不再创建
54
+ // ObjectURL 也不 setThumbnail,避免覆盖更新值与孤儿 URL
55
+ // - inflightRef:同一 asset 在上一轮 effect 还在 await 时,新一轮 effect
56
+ // 不再重复发起(批量导入 assets 频繁变化场景)
34
57
  React.useEffect(() => {
35
58
  if (!runtime) return;
59
+ let cancelled = false;
60
+ const inflight = inflightRef.current;
36
61
  for (const asset of assets) {
37
62
  if (thumbnails[asset.id]) continue;
38
63
  if (asset.type !== 'image') continue;
64
+ if (inflight.has(asset.id)) continue;
65
+ inflight.add(asset.id);
39
66
  (async () => {
40
67
  try {
41
68
  const blob = await runtime.exportAsset(asset.id);
69
+ // effect 已过期或资产已删除:不创建 ObjectURL(零泄漏)
70
+ if (cancelled) return;
42
71
  const url = URL.createObjectURL(blob);
72
+ if (cancelled) {
73
+ // 双重检查:await 与 createObjectURL 之间可能被取消
74
+ URL.revokeObjectURL(url);
75
+ return;
76
+ }
43
77
  setThumbnail(asset.id, url);
44
78
  } catch {
45
- // ignore
79
+ // ignore export 失败
80
+ } finally {
81
+ inflight.delete(asset.id);
46
82
  }
47
83
  })();
48
84
  }
85
+ return () => {
86
+ cancelled = true;
87
+ };
49
88
  // eslint-disable-next-line react-hooks/exhaustive-deps
50
89
  }, [assets, runtime]);
51
90
 
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
+
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
+
52
115
  return (
53
116
  <aside
54
117
  className={`flex w-56 shrink-0 flex-col border-r border-zinc-200 dark:border-zinc-800 ${className}`}
@@ -68,7 +131,11 @@ export function AssetPanel({ className = '' }: AssetPanelProps) {
68
131
  <span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
69
132
  Assets
70
133
  </span>
71
- <span className="text-[11px] tabular-nums text-zinc-400">{assets.length}</span>
134
+ <span className="text-[11px] tabular-nums text-zinc-400">
135
+ {filterType === 'all' && !searchQuery
136
+ ? assets.length
137
+ : `${filteredAssets.length}/${assets.length}`}
138
+ </span>
72
139
  </div>
73
140
 
74
141
  {/* Upload zone */}
@@ -85,30 +152,105 @@ export function AssetPanel({ className = '' }: AssetPanelProps) {
85
152
  className="hidden"
86
153
  onChange={(e) => handleFiles(e.target.files)}
87
154
  />
88
- <svg className="mx-auto h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
89
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
90
- </svg>
155
+ <Icon size={16} className="mx-auto">
156
+ <path d="M12 4.5v15m7.5-7.5h-15" />
157
+ </Icon>
91
158
  <span className="mt-1 block text-[10px] font-medium">
92
159
  {dragging ? 'Drop here' : 'Add files'}
93
160
  </span>
94
161
  </label>
95
162
 
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-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-300'
180
+ : 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
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-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-300'
195
+ : 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
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-zinc-400 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-zinc-100 py-1 pl-6 pr-1.5 text-[10px] text-zinc-700 placeholder:text-zinc-400 focus:outline-none focus:ring-1 focus:ring-indigo-300 dark:bg-zinc-800 dark:text-zinc-200 dark:placeholder:text-zinc-500"
218
+ />
219
+ </div>
220
+ </div>
221
+ )}
222
+
96
223
  {/* Asset list */}
97
224
  <div className="flex-1 overflow-y-auto px-2 pb-2">
98
225
  {assets.length === 0 ? (
99
226
  <p className="px-1 py-4 text-center text-[10px] text-zinc-400">
100
227
  No assets imported
101
228
  </p>
229
+ ) : filteredAssets.length === 0 ? (
230
+ <p className="px-1 py-4 text-center text-[10px] text-zinc-400">
231
+ No assets match filter
232
+ </p>
102
233
  ) : (
103
234
  <ul className="space-y-1">
104
- {assets.map((asset) => {
235
+ {filteredAssets.map((asset) => {
105
236
  const selected = asset.id === selectedAssetId;
106
237
  const thumb = thumbnails[asset.id];
107
238
  return (
108
239
  <li
109
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}`}
110
246
  onClick={() => selectAsset(asset.id)}
111
- className={`group flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-all ${
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-indigo-300 ${
112
254
  selected
113
255
  ? 'bg-indigo-50 ring-1 ring-indigo-200 dark:bg-indigo-950/30 dark:ring-indigo-800'
114
256
  : 'hover:bg-zinc-50 dark:hover:bg-zinc-800'
@@ -135,10 +277,10 @@ export function AssetPanel({ className = '' }: AssetPanelProps) {
135
277
  e.stopPropagation();
136
278
  void removeAsset(asset.id);
137
279
  }}
138
- className="shrink-0 rounded p-0.5 text-zinc-300 opacity-0 transition-all hover:bg-red-50 hover:text-red-500 group-hover:opacity-100 dark:text-zinc-600 dark:hover:bg-red-950/30"
139
- aria-label="Remove asset"
280
+ className="shrink-0 rounded p-0.5 text-zinc-300 opacity-0 transition-all hover:bg-red-50 hover:text-red-500 focus:opacity-100 group-hover:opacity-100 dark:text-zinc-600 dark:hover:bg-red-950/30"
281
+ aria-label={`Remove asset ${asset.metadata.format}`}
140
282
  >
141
- <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
283
+ <Icon size={12}><path d="M6 18L18 6M6 6l12 12" /></Icon>
142
284
  </button>
143
285
  </li>
144
286
  );