@svadmin/ui 0.64.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/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -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/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -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/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/index.d.ts +58 -3
- package/dist/index.js +35 -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,178 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import type { FieldDefinition, CrudOperator } from '@svadmin/core';
|
|
4
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
5
|
+
import * as Popover from './ui/popover/index.js';
|
|
6
|
+
import { Button } from './ui/button/index.js';
|
|
7
|
+
import { Filter, Check, RotateCcw } from '@lucide/svelte';
|
|
8
|
+
import { cn } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
field: FieldDefinition;
|
|
12
|
+
currentValue?: unknown;
|
|
13
|
+
currentOperator?: CrudOperator;
|
|
14
|
+
onapply?: (operator: CrudOperator, value: unknown) => void;
|
|
15
|
+
onclear?: () => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
field,
|
|
21
|
+
currentValue,
|
|
22
|
+
currentOperator = 'contains',
|
|
23
|
+
onapply,
|
|
24
|
+
onclear,
|
|
25
|
+
class: className = '',
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const i18n = useTranslation();
|
|
29
|
+
let open = $state(false);
|
|
30
|
+
let operator = $state<CrudOperator>(untrack(() => currentOperator));
|
|
31
|
+
let filterVal = $state<string>(untrack(() => String(currentValue ?? '')));
|
|
32
|
+
|
|
33
|
+
const hasActiveFilter = $derived(currentValue !== undefined && String(currentValue).trim().length > 0);
|
|
34
|
+
|
|
35
|
+
const applicableOperators = $derived.by(() => {
|
|
36
|
+
switch (field.type) {
|
|
37
|
+
case 'number':
|
|
38
|
+
case 'currency':
|
|
39
|
+
return [
|
|
40
|
+
{ value: 'eq' as const, label: '= Equal' },
|
|
41
|
+
{ value: 'gt' as const, label: '> Greater than' },
|
|
42
|
+
{ value: 'gte' as const, label: '>= Greater equal' },
|
|
43
|
+
{ value: 'lt' as const, label: '< Less than' },
|
|
44
|
+
{ value: 'lte' as const, label: '<= Less equal' },
|
|
45
|
+
];
|
|
46
|
+
case 'date':
|
|
47
|
+
return [
|
|
48
|
+
{ value: 'eq' as const, label: '= On date' },
|
|
49
|
+
{ value: 'gte' as const, label: '>= After or on' },
|
|
50
|
+
{ value: 'lte' as const, label: '<= Before or on' },
|
|
51
|
+
];
|
|
52
|
+
case 'boolean':
|
|
53
|
+
return [
|
|
54
|
+
{ value: 'eq' as const, label: 'Is' },
|
|
55
|
+
];
|
|
56
|
+
default:
|
|
57
|
+
return [
|
|
58
|
+
{ value: 'contains' as const, label: 'Contains' },
|
|
59
|
+
{ value: 'eq' as const, label: 'Equals' },
|
|
60
|
+
{ value: 'ne' as const, label: 'Not equals' },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function handleApply() {
|
|
66
|
+
let finalVal: unknown = filterVal.trim();
|
|
67
|
+
if (field.type === 'number' || field.type === 'currency') {
|
|
68
|
+
const num = Number(finalVal);
|
|
69
|
+
finalVal = isNaN(num) ? finalVal : num;
|
|
70
|
+
}
|
|
71
|
+
onapply?.(operator, finalVal);
|
|
72
|
+
open = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleClear() {
|
|
76
|
+
filterVal = '';
|
|
77
|
+
onclear?.();
|
|
78
|
+
open = false;
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<Popover.Root bind:open>
|
|
83
|
+
<Popover.Trigger>
|
|
84
|
+
{#snippet child({ props })}
|
|
85
|
+
<button
|
|
86
|
+
{...props}
|
|
87
|
+
type="button"
|
|
88
|
+
class={cn(
|
|
89
|
+
'inline-flex items-center justify-center h-6 w-6 rounded-md transition-colors cursor-pointer',
|
|
90
|
+
hasActiveFilter
|
|
91
|
+
? 'bg-primary/15 text-primary'
|
|
92
|
+
: 'text-muted-foreground/60 hover:text-foreground hover:bg-muted/50',
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
aria-label="Filter {field.label}"
|
|
96
|
+
>
|
|
97
|
+
<Filter class="h-3 w-3" />
|
|
98
|
+
</button>
|
|
99
|
+
{/snippet}
|
|
100
|
+
</Popover.Trigger>
|
|
101
|
+
<Popover.Content class="w-64 p-3 text-xs" align="start">
|
|
102
|
+
<div class="space-y-3">
|
|
103
|
+
<div class="font-medium text-foreground pb-1 border-b border-border/50">
|
|
104
|
+
Filter {field.label}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="space-y-1">
|
|
108
|
+
<label class="block text-[11px] text-muted-foreground" for="header_filter_op_{field.key}">Operator</label>
|
|
109
|
+
<select
|
|
110
|
+
id="header_filter_op_{field.key}"
|
|
111
|
+
bind:value={operator}
|
|
112
|
+
class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
113
|
+
>
|
|
114
|
+
{#each applicableOperators as op (op.value)}
|
|
115
|
+
<option value={op.value}>{op.label}</option>
|
|
116
|
+
{/each}
|
|
117
|
+
</select>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="space-y-1">
|
|
121
|
+
<label class="block text-[11px] text-muted-foreground" for="header_filter_val_{field.key}">Value</label>
|
|
122
|
+
{#if field.type === 'select' && field.options}
|
|
123
|
+
<select
|
|
124
|
+
id="header_filter_val_{field.key}"
|
|
125
|
+
bind:value={filterVal}
|
|
126
|
+
class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
127
|
+
>
|
|
128
|
+
<option value="">All</option>
|
|
129
|
+
{#each field.options as opt (opt.value)}
|
|
130
|
+
<option value={String(opt.value)}>{opt.label}</option>
|
|
131
|
+
{/each}
|
|
132
|
+
</select>
|
|
133
|
+
{:else if field.type === 'boolean'}
|
|
134
|
+
<select
|
|
135
|
+
id="header_filter_val_{field.key}"
|
|
136
|
+
bind:value={filterVal}
|
|
137
|
+
class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
138
|
+
>
|
|
139
|
+
<option value="">All</option>
|
|
140
|
+
<option value="true">Yes / True</option>
|
|
141
|
+
<option value="false">No / False</option>
|
|
142
|
+
</select>
|
|
143
|
+
{:else if field.type === 'date'}
|
|
144
|
+
<input
|
|
145
|
+
id="header_filter_val_{field.key}"
|
|
146
|
+
type="date"
|
|
147
|
+
bind:value={filterVal}
|
|
148
|
+
class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
149
|
+
/>
|
|
150
|
+
{:else}
|
|
151
|
+
<input
|
|
152
|
+
id="header_filter_val_{field.key}"
|
|
153
|
+
type="text"
|
|
154
|
+
bind:value={filterVal}
|
|
155
|
+
placeholder="Search {field.label}..."
|
|
156
|
+
class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
157
|
+
/>
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="flex items-center justify-between pt-1 border-t border-border/50">
|
|
162
|
+
<Button
|
|
163
|
+
variant="ghost"
|
|
164
|
+
size="sm"
|
|
165
|
+
class="h-7 px-2 text-xs text-muted-foreground hover:text-foreground gap-1"
|
|
166
|
+
onclick={handleClear}
|
|
167
|
+
>
|
|
168
|
+
<RotateCcw class="h-3 w-3" />
|
|
169
|
+
{i18n.t('common.clear', { defaultValue: 'Clear' })}
|
|
170
|
+
</Button>
|
|
171
|
+
<Button size="sm" class="h-7 px-2.5 text-xs gap-1" onclick={handleApply}>
|
|
172
|
+
<Check class="h-3 w-3" />
|
|
173
|
+
{i18n.t('common.apply', { defaultValue: 'Apply' })}
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</Popover.Content>
|
|
178
|
+
</Popover.Root>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FieldDefinition, CrudOperator } from '@svadmin/core';
|
|
2
|
+
interface Props {
|
|
3
|
+
field: FieldDefinition;
|
|
4
|
+
currentValue?: unknown;
|
|
5
|
+
currentOperator?: CrudOperator;
|
|
6
|
+
onapply?: (operator: CrudOperator, value: unknown) => void;
|
|
7
|
+
onclear?: () => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const ColumnHeaderFilter: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type ColumnHeaderFilter = ReturnType<typeof ColumnHeaderFilter>;
|
|
12
|
+
export default ColumnHeaderFilter;
|