@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.
Files changed (73) hide show
  1. package/dist/components/ActivityFeed.svelte +135 -0
  2. package/dist/components/ActivityFeed.svelte.d.ts +21 -0
  3. package/dist/components/ApprovalActionCard.svelte +248 -0
  4. package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
  5. package/dist/components/CanvasAnnotation.svelte +389 -0
  6. package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
  7. package/dist/components/ColumnHeaderFilter.svelte +178 -0
  8. package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
  9. package/dist/components/ColumnSettings.svelte +198 -0
  10. package/dist/components/ColumnSettings.svelte.d.ts +19 -0
  11. package/dist/components/DecisionTable.svelte +240 -0
  12. package/dist/components/DecisionTable.svelte.d.ts +22 -0
  13. package/dist/components/DraggableRowTable.svelte +117 -0
  14. package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
  15. package/dist/components/DrawerForm.svelte +110 -26
  16. package/dist/components/DrawerForm.svelte.d.ts +20 -8
  17. package/dist/components/EditableTable.svelte +195 -0
  18. package/dist/components/EditableTable.svelte.d.ts +25 -0
  19. package/dist/components/GanttChart.svelte +144 -0
  20. package/dist/components/GanttChart.svelte.d.ts +20 -0
  21. package/dist/components/ImageCropper.svelte +122 -0
  22. package/dist/components/ImageCropper.svelte.d.ts +12 -0
  23. package/dist/components/ImportWizard.svelte +336 -0
  24. package/dist/components/ImportWizard.svelte.d.ts +12 -0
  25. package/dist/components/JsonSchemaForm.svelte +149 -0
  26. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
  27. package/dist/components/KanbanBoard.svelte +212 -0
  28. package/dist/components/KanbanBoard.svelte.d.ts +30 -0
  29. package/dist/components/MasterDetailView.svelte +134 -0
  30. package/dist/components/MasterDetailView.svelte.d.ts +16 -0
  31. package/dist/components/MediaLibraryModal.svelte +195 -0
  32. package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
  33. package/dist/components/MentionsInput.svelte +174 -0
  34. package/dist/components/MentionsInput.svelte.d.ts +21 -0
  35. package/dist/components/ModalForm.svelte +113 -28
  36. package/dist/components/ModalForm.svelte.d.ts +20 -8
  37. package/dist/components/MultiTabKeepAlive.svelte +143 -0
  38. package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
  39. package/dist/components/OfflineSyncBanner.svelte +172 -0
  40. package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
  41. package/dist/components/PdfDocumentViewer.svelte +255 -0
  42. package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
  43. package/dist/components/PivotTable.svelte +140 -0
  44. package/dist/components/PivotTable.svelte.d.ts +16 -0
  45. package/dist/components/PresenceAvatarGroup.svelte +89 -0
  46. package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
  47. package/dist/components/PrintableBill.svelte +149 -0
  48. package/dist/components/PrintableBill.svelte.d.ts +24 -0
  49. package/dist/components/SensitiveDataMask.svelte +125 -0
  50. package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
  51. package/dist/components/SignaturePad.svelte +264 -0
  52. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  53. package/dist/components/SplitPaneLayout.svelte +113 -0
  54. package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
  55. package/dist/components/SpreadsheetView.svelte +363 -0
  56. package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
  57. package/dist/components/StepForm.svelte +180 -0
  58. package/dist/components/StepForm.svelte.d.ts +23 -0
  59. package/dist/components/TableSummary.svelte +110 -0
  60. package/dist/components/TableSummary.svelte.d.ts +20 -0
  61. package/dist/components/TreeTable.svelte +237 -0
  62. package/dist/components/TreeTable.svelte.d.ts +28 -0
  63. package/dist/components/UnsavedChangesPrompt.svelte +44 -0
  64. package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
  65. package/dist/components/VersionDiffViewer.svelte +196 -0
  66. package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
  67. package/dist/components/VirtualTable.svelte +86 -54
  68. package/dist/components/VirtualTable.svelte.d.ts +14 -21
  69. package/dist/components/Watermark.svelte +166 -0
  70. package/dist/components/Watermark.svelte.d.ts +26 -0
  71. package/dist/index.d.ts +58 -3
  72. package/dist/index.js +35 -3
  73. 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;