@svadmin/ui 0.64.0 → 0.65.1

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,89 @@
1
+ <script lang="ts">
2
+ import { Users } from '@lucide/svelte';
3
+ import { cn } from '../utils.js';
4
+
5
+ export interface PresenceUser {
6
+ id: string;
7
+ name: string;
8
+ avatar?: string;
9
+ status?: 'online' | 'idle' | 'editing';
10
+ }
11
+
12
+ interface Props {
13
+ users?: PresenceUser[];
14
+ maxVisible?: number;
15
+ label?: string;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ users = [],
21
+ maxVisible = 4,
22
+ label = 'Viewing now',
23
+ class: className = '',
24
+ }: Props = $props();
25
+
26
+ const visibleUsers = $derived(users.slice(0, maxVisible));
27
+ const hiddenCount = $derived(Math.max(0, users.length - maxVisible));
28
+
29
+ function getInitials(name: string): string {
30
+ return name
31
+ .split(' ')
32
+ .map((n) => n[0])
33
+ .join('')
34
+ .toUpperCase()
35
+ .slice(0, 2);
36
+ }
37
+
38
+ function getStatusIndicator(status?: PresenceUser['status']) {
39
+ switch (status) {
40
+ case 'editing':
41
+ return 'bg-warning ring-warning/30 animate-pulse';
42
+ case 'idle':
43
+ return 'bg-muted-foreground ring-muted-foreground/30';
44
+ default:
45
+ return 'bg-success ring-success/30';
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <div class={cn('inline-flex items-center gap-2 text-xs select-none', className)}>
51
+ {#if label}
52
+ <span class="text-muted-foreground flex items-center gap-1">
53
+ <Users class="h-3.5 w-3.5" />
54
+ {label}:
55
+ </span>
56
+ {/if}
57
+
58
+ <div class="flex items-center -space-x-2 overflow-hidden py-1">
59
+ {#each visibleUsers as user (user.id)}
60
+ <div
61
+ class="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 border-background bg-muted text-[11px] font-semibold text-foreground shadow-xs transition-transform hover:z-10 hover:scale-110"
62
+ title="{user.name} ({user.status ?? 'online'})"
63
+ >
64
+ {#if user.avatar}
65
+ <img src={user.avatar} alt={user.name} class="h-full w-full rounded-full object-cover" />
66
+ {:else}
67
+ <span>{getInitials(user.name)}</span>
68
+ {/if}
69
+
70
+ <!-- Status Dot Indicator -->
71
+ <span
72
+ class={cn(
73
+ 'absolute bottom-0 right-0 h-2 w-2 rounded-full ring-2 ring-background',
74
+ getStatusIndicator(user.status)
75
+ )}
76
+ ></span>
77
+ </div>
78
+ {/each}
79
+
80
+ {#if hiddenCount > 0}
81
+ <div
82
+ class="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 border-background bg-secondary text-[10px] font-medium text-secondary-foreground shadow-xs"
83
+ title="{hiddenCount} more users"
84
+ >
85
+ +{hiddenCount}
86
+ </div>
87
+ {/if}
88
+ </div>
89
+ </div>
@@ -0,0 +1,15 @@
1
+ export interface PresenceUser {
2
+ id: string;
3
+ name: string;
4
+ avatar?: string;
5
+ status?: 'online' | 'idle' | 'editing';
6
+ }
7
+ interface Props {
8
+ users?: PresenceUser[];
9
+ maxVisible?: number;
10
+ label?: string;
11
+ class?: string;
12
+ }
13
+ declare const PresenceAvatarGroup: import("svelte").Component<Props, {}, "">;
14
+ type PresenceAvatarGroup = ReturnType<typeof PresenceAvatarGroup>;
15
+ export default PresenceAvatarGroup;
@@ -0,0 +1,149 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { Printer } from '@lucide/svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ export interface BillItem {
7
+ name: string;
8
+ spec?: string;
9
+ quantity: number;
10
+ unitPrice: number;
11
+ total?: number;
12
+ }
13
+
14
+ interface Props {
15
+ title?: string;
16
+ billNumber: string;
17
+ date: string;
18
+ companyName?: string;
19
+ companyAddress?: string;
20
+ customerName: string;
21
+ customerAddress?: string;
22
+ items?: BillItem[];
23
+ notes?: string;
24
+ issuer?: string;
25
+ onprint?: () => void;
26
+ class?: string;
27
+ }
28
+
29
+ let {
30
+ title = 'INVOICE / RECEIPT',
31
+ billNumber,
32
+ date,
33
+ companyName = 'Enterprise Admin Co., Ltd.',
34
+ companyAddress,
35
+ customerName,
36
+ customerAddress,
37
+ items = [],
38
+ notes,
39
+ issuer,
40
+ onprint,
41
+ class: className = '',
42
+ }: Props = $props();
43
+
44
+ const subtotal = $derived(
45
+ items.reduce((acc, item) => acc + (item.total ?? item.quantity * item.unitPrice), 0)
46
+ );
47
+
48
+ function handlePrint() {
49
+ if (onprint) {
50
+ onprint();
51
+ } else if (typeof window !== 'undefined') {
52
+ window.print();
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <div class={cn('space-y-4 text-xs', className)}>
58
+ <!-- Print Trigger Bar (Hidden during actual print) -->
59
+ <div class="flex items-center justify-between p-3 rounded-xl border border-border bg-card shadow-xs print:hidden">
60
+ <div class="text-muted-foreground">
61
+ Document Ready: <strong class="text-foreground">{billNumber}</strong>
62
+ </div>
63
+ <Button size="sm" class="h-8 text-xs gap-1.5" onclick={handlePrint}>
64
+ <Printer class="h-3.5 w-3.5" />
65
+ Print Document
66
+ </Button>
67
+ </div>
68
+
69
+ <!-- Printable A4 Paper Container -->
70
+ <div class="rounded-xl border border-border bg-card p-8 shadow-xs max-w-3xl mx-auto space-y-6 print:border-none print:shadow-none print:p-0 print:m-0">
71
+ <!-- Header -->
72
+ <div class="flex items-start justify-between border-b-2 border-primary/40 pb-4">
73
+ <div>
74
+ <h2 class="text-xl font-bold text-foreground tracking-tight">{title}</h2>
75
+ <p class="text-muted-foreground font-mono mt-0.5">No: {billNumber}</p>
76
+ </div>
77
+ <div class="text-right space-y-0.5">
78
+ <h3 class="font-semibold text-foreground">{companyName}</h3>
79
+ {#if companyAddress}
80
+ <p class="text-muted-foreground text-[11px]">{companyAddress}</p>
81
+ {/if}
82
+ <p class="text-muted-foreground text-[11px]">Date: {date}</p>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Customer Info -->
87
+ <div class="p-3 rounded-lg bg-muted/20 border border-border/40 space-y-1">
88
+ <span class="text-muted-foreground font-semibold uppercase text-[10px] block">Bill To</span>
89
+ <div class="font-semibold text-foreground">{customerName}</div>
90
+ {#if customerAddress}
91
+ <div class="text-muted-foreground text-[11px]">{customerAddress}</div>
92
+ {/if}
93
+ </div>
94
+
95
+ <!-- Items Table -->
96
+ <table class="w-full text-left border-collapse border border-border/60">
97
+ <thead class="bg-muted/40 font-semibold text-foreground border-b border-border/60">
98
+ <tr>
99
+ <th class="p-2 border-r border-border/40 w-12 text-center">#</th>
100
+ <th class="p-2 border-r border-border/40">Item Description</th>
101
+ <th class="p-2 border-r border-border/40 text-center w-20">Qty</th>
102
+ <th class="p-2 border-r border-border/40 text-right w-24">Unit Price</th>
103
+ <th class="p-2 text-right w-28">Amount</th>
104
+ </tr>
105
+ </thead>
106
+ <tbody class="divide-y divide-border/40">
107
+ {#each items as item, index (index)}
108
+ {@const rowTotal = item.total ?? item.quantity * item.unitPrice}
109
+ <tr>
110
+ <td class="p-2 border-r border-border/40 text-center text-muted-foreground">{index + 1}</td>
111
+ <td class="p-2 border-r border-border/40 font-medium text-foreground">
112
+ {item.name}
113
+ {#if item.spec}
114
+ <span class="block text-[11px] text-muted-foreground">{item.spec}</span>
115
+ {/if}
116
+ </td>
117
+ <td class="p-2 border-r border-border/40 text-center tabular-nums">{item.quantity}</td>
118
+ <td class="p-2 border-r border-border/40 text-right tabular-nums">¥{item.unitPrice.toFixed(2)}</td>
119
+ <td class="p-2 text-right tabular-nums font-semibold text-foreground">¥{rowTotal.toFixed(2)}</td>
120
+ </tr>
121
+ {/each}
122
+ </tbody>
123
+ <tfoot class="border-t-2 border-border font-semibold text-foreground bg-muted/20">
124
+ <tr>
125
+ <td colspan="4" class="p-2.5 text-right border-r border-border/40">Total Summary:</td>
126
+ <td class="p-2.5 text-right text-sm tabular-nums text-primary font-bold">¥{subtotal.toFixed(2)}</td>
127
+ </tr>
128
+ </tfoot>
129
+ </table>
130
+
131
+ <!-- Footer Notes & Signatures -->
132
+ <div class="flex items-end justify-between pt-4 border-t border-border/40 text-xs">
133
+ <div class="space-y-1 max-w-sm">
134
+ {#if notes}
135
+ <span class="font-semibold text-muted-foreground block text-[11px]">Notes:</span>
136
+ <p class="text-muted-foreground">{notes}</p>
137
+ {/if}
138
+ </div>
139
+
140
+ <div class="text-right space-y-4">
141
+ {#if issuer}
142
+ <p class="text-muted-foreground">Issued By: <strong class="text-foreground">{issuer}</strong></p>
143
+ {/if}
144
+ <div class="w-40 border-b border-foreground/40 pt-6"></div>
145
+ <p class="text-[10px] text-muted-foreground text-center">Authorized Signature & Stamp</p>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
@@ -0,0 +1,24 @@
1
+ export interface BillItem {
2
+ name: string;
3
+ spec?: string;
4
+ quantity: number;
5
+ unitPrice: number;
6
+ total?: number;
7
+ }
8
+ interface Props {
9
+ title?: string;
10
+ billNumber: string;
11
+ date: string;
12
+ companyName?: string;
13
+ companyAddress?: string;
14
+ customerName: string;
15
+ customerAddress?: string;
16
+ items?: BillItem[];
17
+ notes?: string;
18
+ issuer?: string;
19
+ onprint?: () => void;
20
+ class?: string;
21
+ }
22
+ declare const PrintableBill: import("svelte").Component<Props, {}, "">;
23
+ type PrintableBill = ReturnType<typeof PrintableBill>;
24
+ export default PrintableBill;
@@ -0,0 +1,125 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { Eye, EyeOff, Copy, Check } from '@lucide/svelte';
4
+ import { Button } from './ui/button/index.js';
5
+ import { cn } from '../utils.js';
6
+
7
+ export type MaskType = 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
8
+
9
+ interface Props {
10
+ value?: string | number | null;
11
+ type?: MaskType;
12
+ maskPattern?: (val: string) => string;
13
+ allowUnmask?: boolean;
14
+ defaultMasked?: boolean;
15
+ onUnmask?: () => void | Promise<void>;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ value = '',
21
+ type = 'phone',
22
+ maskPattern,
23
+ allowUnmask = true,
24
+ defaultMasked = true,
25
+ onUnmask,
26
+ class: className = '',
27
+ }: Props = $props();
28
+
29
+ let isMasked = $state(untrack(() => defaultMasked));
30
+ let isCopied = $state(false);
31
+
32
+ const rawString = $derived(String(value ?? ''));
33
+
34
+ function maskValue(val: string, maskType: MaskType): string {
35
+ if (!val) return '—';
36
+ if (maskPattern) return maskPattern(val);
37
+
38
+ switch (maskType) {
39
+ case 'phone':
40
+ // e.g. 13812345678 -> 138****5678
41
+ return val.length >= 7
42
+ ? `${val.slice(0, 3)}****${val.slice(-4)}`
43
+ : '****';
44
+ case 'id-card':
45
+ // e.g. 110101199001011234 -> 110101********1234
46
+ return val.length >= 10
47
+ ? `${val.slice(0, 6)}********${val.slice(-4)}`
48
+ : '********';
49
+ case 'email': {
50
+ const parts = val.split('@');
51
+ if (parts.length === 2) {
52
+ const name = parts[0];
53
+ const maskedName = name.length > 2 ? `${name[0]}***${name.slice(-1)}` : `${name[0]}***`;
54
+ return `${maskedName}@${parts[1]}`;
55
+ }
56
+ return '***@***.***';
57
+ }
58
+ case 'bank-card':
59
+ return val.length >= 8
60
+ ? `${val.slice(0, 4)} **** **** ${val.slice(-4)}`
61
+ : '**** ****';
62
+ case 'secret':
63
+ return val.length >= 8
64
+ ? `${val.slice(0, 3)}••••••••••••${val.slice(-4)}`
65
+ : '••••••••';
66
+ default:
67
+ return '••••••••';
68
+ }
69
+ }
70
+
71
+ const displayedText = $derived(isMasked ? maskValue(rawString, type) : rawString);
72
+
73
+ async function toggleMask() {
74
+ if (isMasked && onUnmask) {
75
+ await onUnmask();
76
+ }
77
+ isMasked = !isMasked;
78
+ }
79
+
80
+ async function handleCopy() {
81
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
82
+ await navigator.clipboard.writeText(rawString);
83
+ isCopied = true;
84
+ setTimeout(() => {
85
+ isCopied = false;
86
+ }, 1500);
87
+ }
88
+ }
89
+ </script>
90
+
91
+ <div class={cn('inline-flex items-center gap-1.5 font-mono text-xs text-foreground', className)}>
92
+ <span class="truncate select-all">{displayedText}</span>
93
+
94
+ {#if allowUnmask && rawString}
95
+ <Button
96
+ variant="ghost"
97
+ size="icon"
98
+ class="h-5 w-5 text-muted-foreground hover:text-foreground"
99
+ onclick={toggleMask}
100
+ aria-label={isMasked ? 'Reveal sensitive data' : 'Mask sensitive data'}
101
+ >
102
+ {#if isMasked}
103
+ <Eye class="h-3 w-3" />
104
+ {:else}
105
+ <EyeOff class="h-3 w-3" />
106
+ {/if}
107
+ </Button>
108
+ {/if}
109
+
110
+ {#if rawString}
111
+ <Button
112
+ variant="ghost"
113
+ size="icon"
114
+ class="h-5 w-5 text-muted-foreground hover:text-foreground"
115
+ onclick={handleCopy}
116
+ aria-label="Copy to clipboard"
117
+ >
118
+ {#if isCopied}
119
+ <Check class="h-3 w-3 text-success" />
120
+ {:else}
121
+ <Copy class="h-3 w-3" />
122
+ {/if}
123
+ </Button>
124
+ {/if}
125
+ </div>
@@ -0,0 +1,13 @@
1
+ export type MaskType = 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
2
+ interface Props {
3
+ value?: string | number | null;
4
+ type?: MaskType;
5
+ maskPattern?: (val: string) => string;
6
+ allowUnmask?: boolean;
7
+ defaultMasked?: boolean;
8
+ onUnmask?: () => void | Promise<void>;
9
+ class?: string;
10
+ }
11
+ declare const SensitiveDataMask: import("svelte").Component<Props, {}, "">;
12
+ type SensitiveDataMask = ReturnType<typeof SensitiveDataMask>;
13
+ export default SensitiveDataMask;
@@ -0,0 +1,264 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Button } from './ui/button/index.js';
4
+ import { RotateCcw, Trash2, Check, Download } from '@lucide/svelte';
5
+ import { cn } from '../utils.js';
6
+
7
+ interface Props {
8
+ value?: string;
9
+ width?: number;
10
+ height?: number;
11
+ strokeColor?: string;
12
+ strokeWidth?: number;
13
+ disabled?: boolean;
14
+ onchange?: (dataUrl: string) => void;
15
+ onsave?: (dataUrl: string) => void;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ value = $bindable(''),
21
+ width = 500,
22
+ height = 200,
23
+ strokeColor = 'currentColor',
24
+ strokeWidth = 2.5,
25
+ disabled = false,
26
+ onchange,
27
+ onsave,
28
+ class: className = '',
29
+ }: Props = $props();
30
+
31
+ let canvasEl = $state<HTMLCanvasElement | null>(null);
32
+ let isDrawing = $state(false);
33
+ let isEmpty = $state(true);
34
+ let history = $state<ImageData[]>([]);
35
+
36
+ interface Point {
37
+ x: number;
38
+ y: number;
39
+ }
40
+
41
+ let points: Point[] = [];
42
+
43
+ function getCanvasPos(e: MouseEvent | TouchEvent): Point | null {
44
+ if (!canvasEl) return null;
45
+ const rect = canvasEl.getBoundingClientRect();
46
+ const scaleX = canvasEl.width / rect.width;
47
+ const scaleY = canvasEl.height / rect.height;
48
+
49
+ if ('touches' in e) {
50
+ if (e.touches.length === 0) return null;
51
+ const touch = e.touches[0];
52
+ if (!touch) return null;
53
+ return {
54
+ x: (touch.clientX - rect.left) * scaleX,
55
+ y: (touch.clientY - rect.top) * scaleY,
56
+ };
57
+ } else {
58
+ return {
59
+ x: (e.clientX - rect.left) * scaleX,
60
+ y: (e.clientY - rect.top) * scaleY,
61
+ };
62
+ }
63
+ }
64
+
65
+ function startDrawing(e: MouseEvent | TouchEvent) {
66
+ if (disabled || !canvasEl) return;
67
+ const pos = getCanvasPos(e);
68
+ if (!pos) return;
69
+
70
+ const ctx = canvasEl.getContext('2d');
71
+ if (ctx) {
72
+ history = [...history, ctx.getImageData(0, 0, canvasEl.width, canvasEl.height)];
73
+ }
74
+
75
+ isDrawing = true;
76
+ points = [pos];
77
+ }
78
+
79
+ function draw(e: MouseEvent | TouchEvent) {
80
+ if (!isDrawing || disabled || !canvasEl) return;
81
+ const pos = getCanvasPos(e);
82
+ if (!pos) return;
83
+
84
+ points.push(pos);
85
+ const ctx = canvasEl.getContext('2d');
86
+ if (!ctx) return;
87
+
88
+ ctx.strokeStyle = strokeColor === 'currentColor' ? (canvasEl ? getComputedStyle(canvasEl).color : 'rgb(0,0,0)') : strokeColor;
89
+ ctx.lineWidth = strokeWidth;
90
+ ctx.lineCap = 'round';
91
+ ctx.lineJoin = 'round';
92
+
93
+ if (points.length < 3) {
94
+ const b = points[0];
95
+ if (b) {
96
+ ctx.beginPath();
97
+ ctx.arc(b.x, b.y, ctx.lineWidth / 2, 0, Math.PI * 2, !0);
98
+ ctx.fillStyle = ctx.strokeStyle;
99
+ ctx.fill();
100
+ ctx.closePath();
101
+ }
102
+ return;
103
+ }
104
+
105
+ ctx.beginPath();
106
+ const p0 = points[0];
107
+ if (p0) {
108
+ ctx.moveTo(p0.x, p0.y);
109
+ }
110
+ for (let i = 1; i < points.length - 1; i++) {
111
+ const pi = points[i];
112
+ const pi1 = points[i + 1];
113
+ if (pi && pi1) {
114
+ const xc = (pi.x + pi1.x) / 2;
115
+ const yc = (pi.y + pi1.y) / 2;
116
+ ctx.quadraticCurveTo(pi.x, pi.y, xc, yc);
117
+ }
118
+ }
119
+ ctx.stroke();
120
+ isEmpty = false;
121
+ }
122
+
123
+ function stopDrawing() {
124
+ if (!isDrawing || !canvasEl) return;
125
+ isDrawing = false;
126
+ points = [];
127
+
128
+ const dataUrl = canvasEl.toDataURL('image/png');
129
+ value = dataUrl;
130
+ onchange?.(dataUrl);
131
+ }
132
+
133
+ export function clear() {
134
+ if (!canvasEl) return;
135
+ const ctx = canvasEl.getContext('2d');
136
+ if (!ctx) return;
137
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
138
+ history = [];
139
+ isEmpty = true;
140
+ value = '';
141
+ onchange?.('');
142
+ }
143
+
144
+ export function undo() {
145
+ if (!canvasEl || history.length === 0) return;
146
+ const ctx = canvasEl.getContext('2d');
147
+ if (!ctx) return;
148
+ const previous = history[history.length - 1];
149
+ history = history.slice(0, -1);
150
+ if (previous) {
151
+ ctx.putImageData(previous, 0, 0);
152
+ }
153
+ isEmpty = history.length === 0;
154
+ const dataUrl = isEmpty ? '' : canvasEl.toDataURL('image/png');
155
+ value = dataUrl;
156
+ onchange?.(dataUrl);
157
+ }
158
+
159
+ function handleSave() {
160
+ if (isEmpty || !canvasEl) return;
161
+ const dataUrl = canvasEl.toDataURL('image/png');
162
+ onsave?.(dataUrl);
163
+ }
164
+
165
+ function handleDownload() {
166
+ if (isEmpty || !canvasEl) return;
167
+ const a = document.createElement('a');
168
+ a.href = canvasEl.toDataURL('image/png');
169
+ a.download = `signature_${Date.now()}.png`;
170
+ a.click();
171
+ }
172
+
173
+ onMount(() => {
174
+ if (canvasEl && value) {
175
+ const img = new Image();
176
+ img.onload = () => {
177
+ const ctx = canvasEl?.getContext('2d');
178
+ if (ctx && canvasEl) {
179
+ ctx.drawImage(img, 0, 0);
180
+ isEmpty = false;
181
+ }
182
+ };
183
+ img.src = value;
184
+ }
185
+ });
186
+ </script>
187
+
188
+ <div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
189
+ <div class="flex items-center justify-between gap-2 pb-2 border-b border-border/60">
190
+ <div class="font-semibold text-foreground">
191
+ Electronic Signature <span class="text-muted-foreground font-normal">(Sign inside the box)</span>
192
+ </div>
193
+
194
+ <div class="flex items-center gap-1.5">
195
+ <Button
196
+ variant="ghost"
197
+ size="sm"
198
+ class="h-7 text-xs gap-1 text-muted-foreground hover:text-foreground"
199
+ disabled={history.length === 0 || disabled}
200
+ onclick={undo}
201
+ >
202
+ <RotateCcw class="h-3 w-3" />
203
+ Undo
204
+ </Button>
205
+
206
+ <Button
207
+ variant="ghost"
208
+ size="sm"
209
+ class="h-7 text-xs gap-1 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
210
+ disabled={isEmpty || disabled}
211
+ onclick={clear}
212
+ >
213
+ <Trash2 class="h-3 w-3" />
214
+ Clear
215
+ </Button>
216
+
217
+ {#if onsave}
218
+ <Button
219
+ size="sm"
220
+ class="h-7 text-xs gap-1"
221
+ disabled={isEmpty || disabled}
222
+ onclick={handleSave}
223
+ >
224
+ <Check class="h-3 w-3" />
225
+ Confirm
226
+ </Button>
227
+ {:else}
228
+ <Button
229
+ variant="outline"
230
+ size="sm"
231
+ class="h-7 text-xs gap-1"
232
+ disabled={isEmpty || disabled}
233
+ onclick={handleDownload}
234
+ >
235
+ <Download class="h-3 w-3" />
236
+ Download
237
+ </Button>
238
+ {/if}
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Canvas Drawing Board -->
243
+ <div class="relative w-full rounded-lg border-2 border-dashed border-border/80 bg-background overflow-hidden flex items-center justify-center">
244
+ <canvas
245
+ bind:this={canvasEl}
246
+ {width}
247
+ {height}
248
+ class="touch-none cursor-crosshair max-w-full h-auto block"
249
+ onmousedown={startDrawing}
250
+ onmousemove={draw}
251
+ onmouseup={stopDrawing}
252
+ onmouseleave={stopDrawing}
253
+ ontouchstart={(e) => { e.preventDefault(); startDrawing(e); }}
254
+ ontouchmove={(e) => { e.preventDefault(); draw(e); }}
255
+ ontouchend={(e) => { e.preventDefault(); stopDrawing(); }}
256
+ ></canvas>
257
+
258
+ {#if isEmpty}
259
+ <div class="absolute inset-0 flex items-center justify-center pointer-events-none text-muted-foreground/40 font-mono text-xs select-none">
260
+ Sign here with mouse or touch
261
+ </div>
262
+ {/if}
263
+ </div>
264
+ </div>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ value?: string;
3
+ width?: number;
4
+ height?: number;
5
+ strokeColor?: string;
6
+ strokeWidth?: number;
7
+ disabled?: boolean;
8
+ onchange?: (dataUrl: string) => void;
9
+ onsave?: (dataUrl: string) => void;
10
+ class?: string;
11
+ }
12
+ declare const SignaturePad: import("svelte").Component<Props, {
13
+ clear: () => void;
14
+ undo: () => void;
15
+ }, "value">;
16
+ type SignaturePad = ReturnType<typeof SignaturePad>;
17
+ export default SignaturePad;