@svadmin/ui 0.63.0 → 0.65.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.
- package/dist/components/ActivityFeed.svelte +135 -0
- package/dist/components/ActivityFeed.svelte.d.ts +21 -0
- package/dist/components/ApprovalActionCard.svelte +248 -0
- package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
- package/dist/components/AutoTable.svelte +8 -0
- package/dist/components/AutoTable.svelte.d.ts +7 -1
- package/dist/components/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
- package/dist/components/Cascader.svelte +205 -0
- package/dist/components/Cascader.svelte.d.ts +21 -0
- package/dist/components/ColumnHeaderFilter.svelte +178 -0
- package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
- package/dist/components/ColumnSettings.svelte +198 -0
- package/dist/components/ColumnSettings.svelte.d.ts +19 -0
- package/dist/components/DecisionTable.svelte +240 -0
- package/dist/components/DecisionTable.svelte.d.ts +22 -0
- package/dist/components/DraggableRowTable.svelte +117 -0
- package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
- package/dist/components/DrawerForm.svelte +110 -26
- package/dist/components/DrawerForm.svelte.d.ts +20 -8
- package/dist/components/DynamicFormList.svelte +214 -0
- package/dist/components/DynamicFormList.svelte.d.ts +62 -0
- package/dist/components/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -0
- package/dist/components/FieldRenderer.svelte +32 -0
- package/dist/components/FilterBuilder.svelte +310 -0
- package/dist/components/FilterBuilder.svelte.d.ts +23 -0
- package/dist/components/GanttChart.svelte +144 -0
- package/dist/components/GanttChart.svelte.d.ts +20 -0
- package/dist/components/ImageCropper.svelte +122 -0
- package/dist/components/ImageCropper.svelte.d.ts +12 -0
- package/dist/components/ImportWizard.svelte +336 -0
- package/dist/components/ImportWizard.svelte.d.ts +12 -0
- package/dist/components/JsonSchemaForm.svelte +149 -0
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
- package/dist/components/KanbanBoard.svelte +212 -0
- package/dist/components/KanbanBoard.svelte.d.ts +30 -0
- package/dist/components/MasterDetailView.svelte +134 -0
- package/dist/components/MasterDetailView.svelte.d.ts +16 -0
- package/dist/components/MediaLibraryModal.svelte +195 -0
- package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
- package/dist/components/MentionsInput.svelte +174 -0
- package/dist/components/MentionsInput.svelte.d.ts +21 -0
- package/dist/components/ModalForm.svelte +113 -28
- package/dist/components/ModalForm.svelte.d.ts +20 -8
- package/dist/components/MultiTabKeepAlive.svelte +143 -0
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
- package/dist/components/OfflineSyncBanner.svelte +172 -0
- package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
- package/dist/components/PdfDocumentViewer.svelte +255 -0
- package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
- package/dist/components/PivotTable.svelte +140 -0
- package/dist/components/PivotTable.svelte.d.ts +16 -0
- package/dist/components/PresenceAvatarGroup.svelte +89 -0
- package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
- package/dist/components/PrintableBill.svelte +149 -0
- package/dist/components/PrintableBill.svelte.d.ts +24 -0
- package/dist/components/SensitiveDataMask.svelte +125 -0
- package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
- package/dist/components/SignaturePad.svelte +264 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SplitPaneLayout.svelte +113 -0
- package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
- package/dist/components/SpreadsheetView.svelte +363 -0
- package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
- package/dist/components/StepForm.svelte +180 -0
- package/dist/components/StepForm.svelte.d.ts +23 -0
- package/dist/components/TableSummary.svelte +110 -0
- package/dist/components/TableSummary.svelte.d.ts +20 -0
- package/dist/components/Transfer.svelte +352 -0
- package/dist/components/Transfer.svelte.d.ts +28 -0
- package/dist/components/TreeSelect.svelte +307 -0
- package/dist/components/TreeSelect.svelte.d.ts +22 -0
- package/dist/components/TreeTable.svelte +237 -0
- package/dist/components/TreeTable.svelte.d.ts +28 -0
- package/dist/components/UnsavedChangesPrompt.svelte +44 -0
- package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
- package/dist/components/VersionDiffViewer.svelte +196 -0
- package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
- package/dist/components/VirtualTable.svelte +86 -54
- package/dist/components/VirtualTable.svelte.d.ts +14 -21
- package/dist/components/Watermark.svelte +166 -0
- package/dist/components/Watermark.svelte.d.ts +26 -0
- package/dist/components/ui/navigation-menu/index.d.ts +11 -0
- package/dist/components/ui/navigation-menu/index.js +13 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
- package/dist/index.d.ts +68 -3
- package/dist/index.js +42 -3
- package/package.json +2 -2
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import {
|
|
5
|
+
Pencil,
|
|
6
|
+
Square,
|
|
7
|
+
Circle,
|
|
8
|
+
MoveUpRight,
|
|
9
|
+
Type,
|
|
10
|
+
Eraser,
|
|
11
|
+
RotateCcw,
|
|
12
|
+
Trash2,
|
|
13
|
+
Download,
|
|
14
|
+
} from '@lucide/svelte';
|
|
15
|
+
import { cn } from '../utils.js';
|
|
16
|
+
|
|
17
|
+
export type AnnotationTool = 'pen' | 'rect' | 'circle' | 'arrow' | 'text' | 'eraser';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
imageUrl?: string;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
activeTool?: AnnotationTool;
|
|
24
|
+
strokeWidth?: number;
|
|
25
|
+
onchange?: (dataUrl: string) => void;
|
|
26
|
+
onexport?: (dataUrl: string) => void;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
imageUrl = '',
|
|
32
|
+
width = 700,
|
|
33
|
+
height = 450,
|
|
34
|
+
activeTool = $bindable('pen'),
|
|
35
|
+
strokeWidth = 3,
|
|
36
|
+
onchange,
|
|
37
|
+
onexport,
|
|
38
|
+
class: className = '',
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
42
|
+
let isDrawing = $state(false);
|
|
43
|
+
let startX = 0;
|
|
44
|
+
let startY = 0;
|
|
45
|
+
let history = $state<ImageData[]>([]);
|
|
46
|
+
let bgImage: HTMLImageElement | null = null;
|
|
47
|
+
let textInputPrompt = $state('');
|
|
48
|
+
let showTextPrompt = $state(false);
|
|
49
|
+
let textPos = $state({ x: 0, y: 0 });
|
|
50
|
+
|
|
51
|
+
interface Point {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getCanvasPos(e: MouseEvent | TouchEvent): Point | null {
|
|
57
|
+
if (!canvasEl) return null;
|
|
58
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
59
|
+
const scaleX = canvasEl.width / rect.width;
|
|
60
|
+
const scaleY = canvasEl.height / rect.height;
|
|
61
|
+
|
|
62
|
+
if ('touches' in e) {
|
|
63
|
+
if (e.touches.length === 0) return null;
|
|
64
|
+
const touch = e.touches[0];
|
|
65
|
+
if (!touch) return null;
|
|
66
|
+
return {
|
|
67
|
+
x: (touch.clientX - rect.left) * scaleX,
|
|
68
|
+
y: (touch.clientY - rect.top) * scaleY,
|
|
69
|
+
};
|
|
70
|
+
} else {
|
|
71
|
+
return {
|
|
72
|
+
x: (e.clientX - rect.left) * scaleX,
|
|
73
|
+
y: (e.clientY - rect.top) * scaleY,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function saveHistory() {
|
|
79
|
+
if (!canvasEl) return;
|
|
80
|
+
const ctx = canvasEl.getContext('2d');
|
|
81
|
+
if (ctx) {
|
|
82
|
+
history = [...history, ctx.getImageData(0, 0, canvasEl.width, canvasEl.height)];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function drawArrow(ctx: CanvasRenderingContext2D, fromX: number, fromY: number, toX: number, toY: number) {
|
|
87
|
+
const headlen = 12;
|
|
88
|
+
const angle = Math.atan2(toY - fromY, toX - fromX);
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
ctx.moveTo(fromX, fromY);
|
|
91
|
+
ctx.lineTo(toX, toY);
|
|
92
|
+
ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
|
|
93
|
+
ctx.moveTo(toX, toY);
|
|
94
|
+
ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
|
|
95
|
+
ctx.stroke();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startAction(e: MouseEvent | TouchEvent) {
|
|
99
|
+
if (!canvasEl) return;
|
|
100
|
+
const pos = getCanvasPos(e);
|
|
101
|
+
if (!pos) return;
|
|
102
|
+
|
|
103
|
+
if (activeTool === 'text') {
|
|
104
|
+
textPos = pos;
|
|
105
|
+
showTextPrompt = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
saveHistory();
|
|
110
|
+
isDrawing = true;
|
|
111
|
+
startX = pos.x;
|
|
112
|
+
startY = pos.y;
|
|
113
|
+
|
|
114
|
+
const ctx = canvasEl.getContext('2d');
|
|
115
|
+
if (!ctx) return;
|
|
116
|
+
ctx.lineWidth = strokeWidth;
|
|
117
|
+
ctx.lineCap = 'round';
|
|
118
|
+
ctx.lineJoin = 'round';
|
|
119
|
+
|
|
120
|
+
if (activeTool === 'eraser') {
|
|
121
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
122
|
+
} else {
|
|
123
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
124
|
+
ctx.strokeStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (activeTool === 'pen') {
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
ctx.moveTo(pos.x, pos.y);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function handleMove(e: MouseEvent | TouchEvent) {
|
|
134
|
+
if (!isDrawing || !canvasEl) return;
|
|
135
|
+
const pos = getCanvasPos(e);
|
|
136
|
+
if (!pos) return;
|
|
137
|
+
|
|
138
|
+
const ctx = canvasEl.getContext('2d');
|
|
139
|
+
if (!ctx) return;
|
|
140
|
+
|
|
141
|
+
if (activeTool === 'pen' || activeTool === 'eraser') {
|
|
142
|
+
ctx.lineTo(pos.x, pos.y);
|
|
143
|
+
ctx.stroke();
|
|
144
|
+
} else if (activeTool === 'rect' || activeTool === 'circle' || activeTool === 'arrow') {
|
|
145
|
+
const last = history[history.length - 1];
|
|
146
|
+
if (last) {
|
|
147
|
+
ctx.putImageData(last, 0, 0);
|
|
148
|
+
}
|
|
149
|
+
ctx.strokeStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
|
|
150
|
+
ctx.lineWidth = strokeWidth;
|
|
151
|
+
|
|
152
|
+
if (activeTool === 'rect') {
|
|
153
|
+
ctx.strokeRect(startX, startY, pos.x - startX, pos.y - startY);
|
|
154
|
+
} else if (activeTool === 'circle') {
|
|
155
|
+
ctx.beginPath();
|
|
156
|
+
const rx = Math.abs(pos.x - startX) / 2;
|
|
157
|
+
const ry = Math.abs(pos.y - startY) / 2;
|
|
158
|
+
const cx = Math.min(startX, pos.x) + rx;
|
|
159
|
+
const cy = Math.min(startY, pos.y) + ry;
|
|
160
|
+
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
161
|
+
ctx.stroke();
|
|
162
|
+
} else if (activeTool === 'arrow') {
|
|
163
|
+
drawArrow(ctx, startX, startY, pos.x, pos.y);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stopAction() {
|
|
169
|
+
if (!isDrawing || !canvasEl) return;
|
|
170
|
+
isDrawing = false;
|
|
171
|
+
const ctx = canvasEl.getContext('2d');
|
|
172
|
+
if (ctx) {
|
|
173
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
174
|
+
}
|
|
175
|
+
const dataUrl = canvasEl.toDataURL('image/png');
|
|
176
|
+
onchange?.(dataUrl);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function submitText() {
|
|
180
|
+
if (!canvasEl || !textInputPrompt.trim()) {
|
|
181
|
+
showTextPrompt = false;
|
|
182
|
+
textInputPrompt = '';
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
saveHistory();
|
|
186
|
+
const ctx = canvasEl.getContext('2d');
|
|
187
|
+
if (ctx) {
|
|
188
|
+
ctx.fillStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
|
|
189
|
+
ctx.font = 'bold 14px sans-serif';
|
|
190
|
+
ctx.fillText(textInputPrompt.trim(), textPos.x, textPos.y);
|
|
191
|
+
}
|
|
192
|
+
showTextPrompt = false;
|
|
193
|
+
textInputPrompt = '';
|
|
194
|
+
const dataUrl = canvasEl.toDataURL('image/png');
|
|
195
|
+
onchange?.(dataUrl);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function undo() {
|
|
199
|
+
if (!canvasEl || history.length === 0) return;
|
|
200
|
+
const ctx = canvasEl.getContext('2d');
|
|
201
|
+
if (!ctx) return;
|
|
202
|
+
const previous = history[history.length - 1];
|
|
203
|
+
history = history.slice(0, -1);
|
|
204
|
+
if (previous) {
|
|
205
|
+
ctx.putImageData(previous, 0, 0);
|
|
206
|
+
}
|
|
207
|
+
const dataUrl = canvasEl.toDataURL('image/png');
|
|
208
|
+
onchange?.(dataUrl);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function clear() {
|
|
212
|
+
if (!canvasEl) return;
|
|
213
|
+
saveHistory();
|
|
214
|
+
const ctx = canvasEl.getContext('2d');
|
|
215
|
+
if (!ctx) return;
|
|
216
|
+
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
217
|
+
if (bgImage) {
|
|
218
|
+
ctx.drawImage(bgImage, 0, 0, canvasEl.width, canvasEl.height);
|
|
219
|
+
}
|
|
220
|
+
const dataUrl = canvasEl.toDataURL('image/png');
|
|
221
|
+
onchange?.(dataUrl);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleExport() {
|
|
225
|
+
if (!canvasEl) return;
|
|
226
|
+
const dataUrl = canvasEl.toDataURL('image/png');
|
|
227
|
+
onexport?.(dataUrl);
|
|
228
|
+
|
|
229
|
+
const a = document.createElement('a');
|
|
230
|
+
a.href = dataUrl;
|
|
231
|
+
a.download = `annotation_${Date.now()}.png`;
|
|
232
|
+
a.click();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function redrawBg() {
|
|
236
|
+
if (!canvasEl || !imageUrl) return;
|
|
237
|
+
bgImage = new Image();
|
|
238
|
+
bgImage.crossOrigin = 'anonymous';
|
|
239
|
+
bgImage.onload = () => {
|
|
240
|
+
if (!canvasEl) return;
|
|
241
|
+
const ctx = canvasEl.getContext('2d');
|
|
242
|
+
if (ctx) {
|
|
243
|
+
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
244
|
+
if (bgImage) ctx.drawImage(bgImage, 0, 0, canvasEl.width, canvasEl.height);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
bgImage.src = imageUrl;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
onMount(() => {
|
|
251
|
+
redrawBg();
|
|
252
|
+
});
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
|
|
256
|
+
<!-- Header & Toolbar -->
|
|
257
|
+
<div class="flex flex-wrap items-center justify-between gap-2 pb-2 border-b border-border/60">
|
|
258
|
+
<div class="flex items-center gap-1 bg-muted/40 p-1 rounded-lg border border-border/60">
|
|
259
|
+
<Button
|
|
260
|
+
variant={activeTool === 'pen' ? 'default' : 'ghost'}
|
|
261
|
+
size="sm"
|
|
262
|
+
class="h-7 w-7 p-0"
|
|
263
|
+
onclick={() => { activeTool = 'pen'; }}
|
|
264
|
+
title="Pen"
|
|
265
|
+
>
|
|
266
|
+
<Pencil class="h-3.5 w-3.5" />
|
|
267
|
+
</Button>
|
|
268
|
+
|
|
269
|
+
<Button
|
|
270
|
+
variant={activeTool === 'rect' ? 'default' : 'ghost'}
|
|
271
|
+
size="sm"
|
|
272
|
+
class="h-7 w-7 p-0"
|
|
273
|
+
onclick={() => { activeTool = 'rect'; }}
|
|
274
|
+
title="Rectangle"
|
|
275
|
+
>
|
|
276
|
+
<Square class="h-3.5 w-3.5" />
|
|
277
|
+
</Button>
|
|
278
|
+
|
|
279
|
+
<Button
|
|
280
|
+
variant={activeTool === 'circle' ? 'default' : 'ghost'}
|
|
281
|
+
size="sm"
|
|
282
|
+
class="h-7 w-7 p-0"
|
|
283
|
+
onclick={() => { activeTool = 'circle'; }}
|
|
284
|
+
title="Circle"
|
|
285
|
+
>
|
|
286
|
+
<Circle class="h-3.5 w-3.5" />
|
|
287
|
+
</Button>
|
|
288
|
+
|
|
289
|
+
<Button
|
|
290
|
+
variant={activeTool === 'arrow' ? 'default' : 'ghost'}
|
|
291
|
+
size="sm"
|
|
292
|
+
class="h-7 w-7 p-0"
|
|
293
|
+
onclick={() => { activeTool = 'arrow'; }}
|
|
294
|
+
title="Arrow"
|
|
295
|
+
>
|
|
296
|
+
<MoveUpRight class="h-3.5 w-3.5" />
|
|
297
|
+
</Button>
|
|
298
|
+
|
|
299
|
+
<Button
|
|
300
|
+
variant={activeTool === 'text' ? 'default' : 'ghost'}
|
|
301
|
+
size="sm"
|
|
302
|
+
class="h-7 w-7 p-0"
|
|
303
|
+
onclick={() => { activeTool = 'text'; }}
|
|
304
|
+
title="Text Note"
|
|
305
|
+
>
|
|
306
|
+
<Type class="h-3.5 w-3.5" />
|
|
307
|
+
</Button>
|
|
308
|
+
|
|
309
|
+
<Button
|
|
310
|
+
variant={activeTool === 'eraser' ? 'default' : 'ghost'}
|
|
311
|
+
size="sm"
|
|
312
|
+
class="h-7 w-7 p-0"
|
|
313
|
+
onclick={() => { activeTool = 'eraser'; }}
|
|
314
|
+
title="Eraser"
|
|
315
|
+
>
|
|
316
|
+
<Eraser class="h-3.5 w-3.5" />
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div class="flex items-center gap-1.5">
|
|
321
|
+
<Button
|
|
322
|
+
variant="ghost"
|
|
323
|
+
size="sm"
|
|
324
|
+
class="h-7 text-xs gap-1 text-muted-foreground hover:text-foreground"
|
|
325
|
+
disabled={history.length === 0}
|
|
326
|
+
onclick={undo}
|
|
327
|
+
>
|
|
328
|
+
<RotateCcw class="h-3 w-3" />
|
|
329
|
+
Undo
|
|
330
|
+
</Button>
|
|
331
|
+
|
|
332
|
+
<Button
|
|
333
|
+
variant="ghost"
|
|
334
|
+
size="sm"
|
|
335
|
+
class="h-7 text-xs gap-1 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
|
336
|
+
onclick={clear}
|
|
337
|
+
>
|
|
338
|
+
<Trash2 class="h-3 w-3" />
|
|
339
|
+
Clear
|
|
340
|
+
</Button>
|
|
341
|
+
|
|
342
|
+
<Button
|
|
343
|
+
variant="outline"
|
|
344
|
+
size="sm"
|
|
345
|
+
class="h-7 text-xs gap-1"
|
|
346
|
+
onclick={handleExport}
|
|
347
|
+
>
|
|
348
|
+
<Download class="h-3 w-3" />
|
|
349
|
+
Export
|
|
350
|
+
</Button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<!-- Canvas Container -->
|
|
355
|
+
<div class="relative w-full rounded-lg border border-border/80 bg-muted/20 overflow-hidden flex items-center justify-center">
|
|
356
|
+
<canvas
|
|
357
|
+
bind:this={canvasEl}
|
|
358
|
+
{width}
|
|
359
|
+
{height}
|
|
360
|
+
class="touch-none cursor-crosshair max-w-full h-auto block bg-card"
|
|
361
|
+
onmousedown={startAction}
|
|
362
|
+
onmousemove={handleMove}
|
|
363
|
+
onmouseup={stopAction}
|
|
364
|
+
onmouseleave={stopAction}
|
|
365
|
+
ontouchstart={(e) => { e.preventDefault(); startAction(e); }}
|
|
366
|
+
ontouchmove={(e) => { e.preventDefault(); handleMove(e); }}
|
|
367
|
+
ontouchend={(e) => { e.preventDefault(); stopAction(); }}
|
|
368
|
+
></canvas>
|
|
369
|
+
|
|
370
|
+
{#if showTextPrompt}
|
|
371
|
+
<div
|
|
372
|
+
class="absolute z-50 p-2 rounded-lg border border-border bg-popover shadow-md flex items-center gap-1.5"
|
|
373
|
+
style={`left: ${Math.min(textPos.x, width - 180)}px; top: ${Math.min(textPos.y, height - 50)}px;`}
|
|
374
|
+
>
|
|
375
|
+
<input
|
|
376
|
+
type="text"
|
|
377
|
+
bind:value={textInputPrompt}
|
|
378
|
+
placeholder="Annotation text..."
|
|
379
|
+
class="h-7 w-36 rounded border border-input bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
380
|
+
onkeydown={(e) => {
|
|
381
|
+
if (e.key === 'Enter') submitText();
|
|
382
|
+
else if (e.key === 'Escape') showTextPrompt = false;
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
<Button size="sm" class="h-7 text-xs px-2" onclick={submitText}>Add</Button>
|
|
386
|
+
</div>
|
|
387
|
+
{/if}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AnnotationTool = 'pen' | 'rect' | 'circle' | 'arrow' | 'text' | 'eraser';
|
|
2
|
+
interface Props {
|
|
3
|
+
imageUrl?: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
activeTool?: AnnotationTool;
|
|
7
|
+
strokeWidth?: number;
|
|
8
|
+
onchange?: (dataUrl: string) => void;
|
|
9
|
+
onexport?: (dataUrl: string) => void;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const CanvasAnnotation: import("svelte").Component<Props, {
|
|
13
|
+
undo: () => void;
|
|
14
|
+
clear: () => void;
|
|
15
|
+
}, "activeTool">;
|
|
16
|
+
type CanvasAnnotation = ReturnType<typeof CanvasAnnotation>;
|
|
17
|
+
export default CanvasAnnotation;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronRight, Check, X, ChevronsUpDown } from '@lucide/svelte';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import * as Popover from './ui/popover/index.js';
|
|
5
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
|
+
|
|
7
|
+
export interface CascaderOption {
|
|
8
|
+
value: string | number;
|
|
9
|
+
label: string;
|
|
10
|
+
children?: CascaderOption[];
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
isLeaf?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const i18n = useTranslation();
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
options?: CascaderOption[];
|
|
19
|
+
value?: (string | number)[];
|
|
20
|
+
separator?: string;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
changeOnSelect?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
allowClear?: boolean;
|
|
25
|
+
class?: string;
|
|
26
|
+
onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
options = [],
|
|
31
|
+
value = $bindable(undefined),
|
|
32
|
+
separator = ' / ',
|
|
33
|
+
placeholder,
|
|
34
|
+
changeOnSelect = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
allowClear = true,
|
|
37
|
+
class: className,
|
|
38
|
+
onchange,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
let open = $state(false);
|
|
42
|
+
// Active selected path while navigating panels
|
|
43
|
+
let activePath = $state<(string | number)[]>([]);
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (value && Array.isArray(value)) {
|
|
47
|
+
activePath = [...value];
|
|
48
|
+
} else {
|
|
49
|
+
activePath = [];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Calculate columns based on activePath
|
|
54
|
+
const columns = $derived.by<CascaderOption[][]>(() => {
|
|
55
|
+
const cols: CascaderOption[][] = [options];
|
|
56
|
+
let currentOptions = options;
|
|
57
|
+
|
|
58
|
+
for (const val of activePath) {
|
|
59
|
+
const selectedOpt = currentOptions.find((o) => o.value === val);
|
|
60
|
+
if (selectedOpt && selectedOpt.children && selectedOpt.children.length > 0) {
|
|
61
|
+
cols.push(selectedOpt.children);
|
|
62
|
+
currentOptions = selectedOpt.children;
|
|
63
|
+
} else {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return cols;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Selected options objects corresponding to value
|
|
72
|
+
const selectedOptions = $derived.by<CascaderOption[]>(() => {
|
|
73
|
+
if (!value || !Array.isArray(value) || value.length === 0) return [];
|
|
74
|
+
const opts: CascaderOption[] = [];
|
|
75
|
+
let currentLevel = options;
|
|
76
|
+
|
|
77
|
+
for (const val of value) {
|
|
78
|
+
const found = currentLevel.find((o) => o.value === val);
|
|
79
|
+
if (found) {
|
|
80
|
+
opts.push(found);
|
|
81
|
+
currentLevel = found.children ?? [];
|
|
82
|
+
} else {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return opts;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const displayLabel = $derived.by(() => {
|
|
90
|
+
if (selectedOptions.length === 0) return '';
|
|
91
|
+
return selectedOptions.map((o) => o.label).join(separator);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function handleSelectOption(colIndex: number, option: CascaderOption) {
|
|
95
|
+
if (option.disabled) return;
|
|
96
|
+
|
|
97
|
+
const nextPath = [...activePath.slice(0, colIndex), option.value];
|
|
98
|
+
activePath = nextPath;
|
|
99
|
+
|
|
100
|
+
const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf);
|
|
101
|
+
|
|
102
|
+
if (!hasChildren || changeOnSelect) {
|
|
103
|
+
value = nextPath;
|
|
104
|
+
const currentSelected = getOptionsForPath(nextPath);
|
|
105
|
+
onchange?.(nextPath, currentSelected);
|
|
106
|
+
if (!hasChildren) {
|
|
107
|
+
open = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getOptionsForPath(path: (string | number)[]): CascaderOption[] {
|
|
113
|
+
const result: CascaderOption[] = [];
|
|
114
|
+
let currentLevel = options;
|
|
115
|
+
for (const val of path) {
|
|
116
|
+
const found = currentLevel.find((o) => o.value === val);
|
|
117
|
+
if (found) {
|
|
118
|
+
result.push(found);
|
|
119
|
+
currentLevel = found.children ?? [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clearAll(event?: MouseEvent) {
|
|
126
|
+
event?.stopPropagation();
|
|
127
|
+
value = undefined;
|
|
128
|
+
activePath = [];
|
|
129
|
+
onchange?.(undefined, []);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<div class={cn('relative w-full', className)} data-testid="cascader">
|
|
134
|
+
<Popover.Root bind:open>
|
|
135
|
+
<Popover.Trigger class="w-full">
|
|
136
|
+
{#snippet child({ props })}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
{...props}
|
|
140
|
+
{disabled}
|
|
141
|
+
class={cn(
|
|
142
|
+
'flex min-h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-left',
|
|
143
|
+
open && 'ring-2 ring-ring ring-offset-2'
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<span class={cn('truncate', displayLabel ? 'text-foreground' : 'text-muted-foreground')}>
|
|
147
|
+
{displayLabel || (placeholder ?? (i18n.t('field.selectPlaceholder', undefined) ?? '请选择...'))}
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
<div class="flex items-center gap-1 shrink-0 ml-1 text-muted-foreground">
|
|
151
|
+
{#if allowClear && displayLabel && !disabled}
|
|
152
|
+
<span
|
|
153
|
+
role="button"
|
|
154
|
+
tabindex="0"
|
|
155
|
+
class="hover:text-foreground cursor-pointer p-0.5"
|
|
156
|
+
onclick={clearAll}
|
|
157
|
+
onkeydown={(e) => {
|
|
158
|
+
if (e.key === 'Enter') clearAll(e as never);
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<X class="h-3.5 w-3.5" />
|
|
162
|
+
</span>
|
|
163
|
+
{/if}
|
|
164
|
+
<ChevronsUpDown class="h-3.5 w-3.5 opacity-50" />
|
|
165
|
+
</div>
|
|
166
|
+
</button>
|
|
167
|
+
{/snippet}
|
|
168
|
+
</Popover.Trigger>
|
|
169
|
+
|
|
170
|
+
<Popover.Content class="w-auto min-w-[180px] p-0 shadow-lg border border-border" align="start">
|
|
171
|
+
<div class="flex divide-x divide-border max-h-64 overflow-x-auto">
|
|
172
|
+
{#each columns as columnOptions, colIndex (colIndex)}
|
|
173
|
+
<div class="w-40 min-w-[140px] max-h-64 overflow-y-auto p-1 space-y-0.5">
|
|
174
|
+
{#each columnOptions as option (option.value)}
|
|
175
|
+
{@const isSelectedInPath = activePath[colIndex] === option.value}
|
|
176
|
+
{@const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf)}
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
class={cn(
|
|
180
|
+
'flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-xs text-left transition-colors',
|
|
181
|
+
isSelectedInPath
|
|
182
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
183
|
+
: 'hover:bg-muted/60 text-foreground',
|
|
184
|
+
option.disabled && 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
185
|
+
)}
|
|
186
|
+
onclick={() => handleSelectOption(colIndex, option)}
|
|
187
|
+
>
|
|
188
|
+
<span class="truncate flex-1">{option.label}</span>
|
|
189
|
+
{#if hasChildren}
|
|
190
|
+
<ChevronRight class="h-3.5 w-3.5 shrink-0 opacity-60 ml-1" />
|
|
191
|
+
{:else if isSelectedInPath && value && value[colIndex] === option.value}
|
|
192
|
+
<Check class="h-3.5 w-3.5 shrink-0 text-primary ml-1" />
|
|
193
|
+
{/if}
|
|
194
|
+
</button>
|
|
195
|
+
{:else}
|
|
196
|
+
<div class="py-4 text-center text-xs text-muted-foreground">
|
|
197
|
+
{i18n.t('common.noData', undefined) ?? '无数据'}
|
|
198
|
+
</div>
|
|
199
|
+
{/each}
|
|
200
|
+
</div>
|
|
201
|
+
{/each}
|
|
202
|
+
</div>
|
|
203
|
+
</Popover.Content>
|
|
204
|
+
</Popover.Root>
|
|
205
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CascaderOption {
|
|
2
|
+
value: string | number;
|
|
3
|
+
label: string;
|
|
4
|
+
children?: CascaderOption[];
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
isLeaf?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
options?: CascaderOption[];
|
|
10
|
+
value?: (string | number)[];
|
|
11
|
+
separator?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
changeOnSelect?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
allowClear?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const Cascader: import("svelte").Component<Props, {}, "value">;
|
|
20
|
+
type Cascader = ReturnType<typeof Cascader>;
|
|
21
|
+
export default Cascader;
|