@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
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* 通过 `accept` 白名单做 MIME 类型校验,拒绝类型时给出视觉反馈 + 警告。
|
|
6
6
|
*
|
|
7
7
|
* 工作机制:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* 1. 监听 window dragenter / dragover / dragleave / drop 事件
|
|
9
|
+
* 2. dragenter 显示遮罩,dragleave(离开 window)隐藏
|
|
10
|
+
* 3. drop 时校验文件类型 → 合法则 importFiles,非法则提示拒绝原因
|
|
11
11
|
*
|
|
12
12
|
* MIME 校验:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* - 默认接受所有 image/* 类型(以及 video/audio/pdf 等,与 workspace 多媒体定位对齐)
|
|
14
|
+
* - 拒绝类型在遮罩中央显示红色提示,并阻止导入
|
|
15
15
|
*
|
|
16
16
|
* 与 AssetPanel / Canvas 内置的局部 dropzone 互不冲突:它们各自处理自己的
|
|
17
17
|
* drop 事件并 `stopPropagation()`,本组件的 window drop 监听不会触发。
|
|
@@ -33,9 +33,9 @@ import { Icon } from '@lokvis/ui-core';
|
|
|
33
33
|
import { useWorkspaceStore } from '../store/index.js';
|
|
34
34
|
|
|
35
35
|
export interface GlobalDropzoneProps {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/** 接受的 MIME 类型(逗号分隔),默认接受图片/视频/音频/PDF */
|
|
37
|
+
accept?: string;
|
|
38
|
+
className?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** 默认接受的类型:与 Workspace 多媒体定位对齐 */
|
|
@@ -43,178 +43,178 @@ const DEFAULT_ACCEPT = 'image/*,video/*,audio/*,application/pdf';
|
|
|
43
43
|
|
|
44
44
|
/** 检查单个文件是否符合 accept 白名单 */
|
|
45
45
|
function isFileAccepted(file: File, accept: string): boolean {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
if (!accept) return true;
|
|
47
|
+
const patterns = accept.split(',').map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
48
|
+
if (patterns.length === 0) return true;
|
|
49
|
+
const mime = file.type.toLowerCase();
|
|
50
|
+
const name = file.name.toLowerCase();
|
|
51
|
+
return patterns.some((p) => {
|
|
52
|
+
// image/* → image/(prefix match)
|
|
53
|
+
if (p.endsWith('/*')) {
|
|
54
|
+
return mime.startsWith(p.slice(0, -1));
|
|
55
|
+
}
|
|
56
|
+
// image/jpeg → exact mime match
|
|
57
|
+
if (p.includes('/')) {
|
|
58
|
+
return mime === p;
|
|
59
|
+
}
|
|
60
|
+
// .png → extension match
|
|
61
|
+
if (p.startsWith('.')) {
|
|
62
|
+
return name.endsWith(p);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function GlobalDropzone({
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
accept = DEFAULT_ACCEPT,
|
|
70
|
+
className = '',
|
|
71
71
|
}: GlobalDropzoneProps) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const importFiles = useWorkspaceStore((s) => s.importFiles);
|
|
73
|
+
const setStatus = useWorkspaceStore((s) => s.setStatus);
|
|
74
|
+
const setError = useWorkspaceStore((s) => s.setError);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// dragCounter:用计数器而非布尔,避免子元素 dragenter/dragleave 触发抖动
|
|
77
|
+
const [dragCounter, setDragCounter] = React.useState(0);
|
|
78
|
+
const [rejectedFiles, setRejectedFiles] = React.useState<string[]>([]);
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
// 阻止浏览器默认行为(打开文件 / 下载),否则 drop 不会触发
|
|
82
|
+
const preventDefault = (e: DragEvent) => {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
};
|
|
86
|
+
// dragenter:进入窗口,计数 +1
|
|
87
|
+
const onDragEnter = (e: DragEvent) => {
|
|
88
|
+
preventDefault(e);
|
|
89
|
+
if (e.dataTransfer?.types?.includes('Files')) {
|
|
90
|
+
setDragCounter((c) => c + 1);
|
|
91
|
+
setRejectedFiles([]);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// dragover:必须 preventDefault 才能触发 drop
|
|
95
|
+
const onDragOver = (e: DragEvent) => {
|
|
96
|
+
preventDefault(e);
|
|
97
|
+
if (e.dataTransfer) {
|
|
98
|
+
// 显示 drop 效果
|
|
99
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// dragleave:离开窗口,计数 -1
|
|
103
|
+
const onDragLeave = (e: DragEvent) => {
|
|
104
|
+
preventDefault(e);
|
|
105
|
+
// 只在离开 window 时(relatedTarget 为 null 或 document.documentElement)减计数
|
|
106
|
+
// relatedTarget 为 null 表示离开文档边界
|
|
107
|
+
if (e.relatedTarget === null) {
|
|
108
|
+
setDragCounter(0);
|
|
109
|
+
setRejectedFiles([]);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// drop:校验 + 导入
|
|
113
|
+
const onDrop = (e: DragEvent) => {
|
|
114
|
+
// 兜底:子组件(Canvas / AssetPanel)若已处理 drop 并 preventDefault(),
|
|
115
|
+
// 表明文件已导入,本组件跳过避免双重导入。子组件仍需 stopPropagation()
|
|
116
|
+
// 防止事件冒泡到 window,这里 defaultPrevented 是双保险。
|
|
117
|
+
if (e.defaultPrevented) {
|
|
118
|
+
setDragCounter(0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
preventDefault(e);
|
|
122
|
+
setDragCounter(0);
|
|
123
|
+
const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
|
|
124
|
+
if (files.length === 0) return;
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
const accepted: File[] = [];
|
|
127
|
+
const rejected: string[] = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
if (isFileAccepted(file, accept)) {
|
|
130
|
+
accepted.push(file);
|
|
131
|
+
} else {
|
|
132
|
+
rejected.push(`${file.name} (${file.type || 'unknown'})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (rejected.length > 0) {
|
|
136
|
+
setRejectedFiles(rejected);
|
|
137
|
+
// 5 秒后自动清空拒绝提示
|
|
138
|
+
setTimeout(() => setRejectedFiles([]), 5000);
|
|
139
|
+
}
|
|
140
|
+
if (accepted.length > 0) {
|
|
141
|
+
void importFiles(accepted);
|
|
142
|
+
} else {
|
|
143
|
+
setStatus(`Rejected ${rejected.length} file(s) — unsupported type`);
|
|
144
|
+
setError(null);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
148
|
+
window.addEventListener('dragenter', onDragEnter);
|
|
149
|
+
window.addEventListener('dragover', onDragOver);
|
|
150
|
+
window.addEventListener('dragleave', onDragLeave);
|
|
151
|
+
window.addEventListener('drop', onDrop);
|
|
152
|
+
return () => {
|
|
153
|
+
window.removeEventListener('dragenter', onDragEnter);
|
|
154
|
+
window.removeEventListener('dragover', onDragOver);
|
|
155
|
+
window.removeEventListener('dragleave', onDragLeave);
|
|
156
|
+
window.removeEventListener('drop', onDrop);
|
|
157
|
+
};
|
|
158
|
+
}, [accept, importFiles, setStatus, setError]);
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const isDragging = dragCounter > 0;
|
|
161
|
+
const hasRejections = rejectedFiles.length > 0;
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
if (!isDragging && !hasRejections) return null;
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
className={`pointer-events-none fixed inset-0 z-40 flex items-center justify-center p-8 ${className}`}
|
|
168
|
+
role="alertdialog"
|
|
169
|
+
aria-live="polite"
|
|
170
|
+
>
|
|
171
|
+
{/* 遮罩 */}
|
|
172
|
+
<div className="absolute inset-0 bg-[var(--lokvis-primary)]/10/80 backdrop-blur-sm" />
|
|
173
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
|
-
|
|
174
|
+
{/* 内容 */}
|
|
175
|
+
<div
|
|
176
|
+
className={`relative w-full max-w-md rounded-2xl border-2 border-dashed p-8 text-center bg-[var(--lokvis-surface)] shadow-2xl ${
|
|
177
|
+
hasRejections ? 'border-[var(--lokvis-danger)]' : 'border-[var(--lokvis-primary)]'
|
|
178
|
+
}`}
|
|
179
|
+
>
|
|
180
|
+
{hasRejections ? (
|
|
181
|
+
<>
|
|
182
|
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-[var(--lokvis-danger)]/15">
|
|
183
|
+
<Icon size={24} className="text-[var(--lokvis-danger)]" strokeWidth={2}>
|
|
184
|
+
<circle cx="12" cy="12" r="10" />
|
|
185
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
186
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
187
|
+
</Icon>
|
|
188
|
+
</div>
|
|
189
|
+
<p className="mt-3 text-sm font-semibold text-[var(--lokvis-danger)]">
|
|
190
|
+
Unsupported file type
|
|
191
|
+
</p>
|
|
192
|
+
<p className="mt-1.5 text-xs text-[var(--lokvis-danger)]/80">
|
|
193
|
+
{rejectedFiles.join(', ')}
|
|
194
|
+
</p>
|
|
195
|
+
<p className="mt-2 text-[10px] text-[var(--lokvis-fg-muted)]">
|
|
196
|
+
Accepted: {accept || 'all files'}
|
|
197
|
+
</p>
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-[var(--lokvis-primary)]/15">
|
|
202
|
+
<Icon size={24} className="text-[var(--lokvis-primary)]" strokeWidth={1.5}>
|
|
203
|
+
<path d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" />
|
|
204
|
+
</Icon>
|
|
205
|
+
</div>
|
|
206
|
+
<p className="mt-3 text-sm font-semibold text-[var(--lokvis-primary)]">
|
|
207
|
+
Drop files to import
|
|
208
|
+
</p>
|
|
209
|
+
<p className="mt-1 text-xs text-[var(--lokvis-fg-muted)]">
|
|
210
|
+
Files are processed locally in your browser
|
|
211
|
+
</p>
|
|
212
|
+
<p className="mt-2 text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
213
|
+
Accepted: {accept || 'all files'}
|
|
214
|
+
</p>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
220
|
}
|