@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,172 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import { Badge } from './ui/badge/index.js';
|
|
5
|
+
import {
|
|
6
|
+
Wifi,
|
|
7
|
+
WifiOff,
|
|
8
|
+
RefreshCw,
|
|
9
|
+
CheckCircle2,
|
|
10
|
+
Clock,
|
|
11
|
+
AlertCircle,
|
|
12
|
+
} from '@lucide/svelte';
|
|
13
|
+
import { cn } from '../utils.js';
|
|
14
|
+
|
|
15
|
+
export interface PendingMutation {
|
|
16
|
+
id: string;
|
|
17
|
+
action: string; // e.g. "update", "create", "delete"
|
|
18
|
+
resource: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
status?: 'pending' | 'syncing' | 'failed' | 'synced';
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
isOnline?: boolean;
|
|
26
|
+
pendingMutations?: PendingMutation[];
|
|
27
|
+
autoSync?: boolean;
|
|
28
|
+
onsync?: (mutations: PendingMutation[]) => void | Promise<void>;
|
|
29
|
+
onretry?: (mutationId: string) => void;
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
isOnline = $bindable(true),
|
|
35
|
+
pendingMutations = $bindable([]),
|
|
36
|
+
autoSync = true,
|
|
37
|
+
onsync,
|
|
38
|
+
onretry,
|
|
39
|
+
class: className = '',
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
let isSyncing = $state(false);
|
|
43
|
+
let expanded = $state(false);
|
|
44
|
+
|
|
45
|
+
async function triggerSync() {
|
|
46
|
+
if (isSyncing || pendingMutations.length === 0) return;
|
|
47
|
+
isSyncing = true;
|
|
48
|
+
try {
|
|
49
|
+
await onsync?.(pendingMutations);
|
|
50
|
+
} finally {
|
|
51
|
+
isSyncing = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMount(() => {
|
|
56
|
+
if (typeof window !== 'undefined') {
|
|
57
|
+
const handleOnline = () => {
|
|
58
|
+
isOnline = true;
|
|
59
|
+
if (autoSync && pendingMutations.length > 0) {
|
|
60
|
+
triggerSync();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const handleOffline = () => {
|
|
64
|
+
isOnline = false;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
window.addEventListener('online', handleOnline);
|
|
68
|
+
window.addEventListener('offline', handleOffline);
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener('online', handleOnline);
|
|
72
|
+
window.removeEventListener('offline', handleOffline);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
{#if !isOnline || pendingMutations.length > 0}
|
|
79
|
+
<div
|
|
80
|
+
class={cn(
|
|
81
|
+
'rounded-xl border shadow-xs text-xs p-3 transition-all',
|
|
82
|
+
!isOnline
|
|
83
|
+
? 'bg-warning/15 border-warning/30 text-warning-foreground'
|
|
84
|
+
: 'bg-muted/40 border-border/80 text-foreground',
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
89
|
+
<!-- Status Indicator -->
|
|
90
|
+
<div class="flex items-center gap-2">
|
|
91
|
+
{#if !isOnline}
|
|
92
|
+
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-warning/20 text-warning">
|
|
93
|
+
<WifiOff class="h-3.5 w-3.5" />
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<div class="font-semibold text-foreground">Offline Mode</div>
|
|
97
|
+
<div class="text-[11px] text-muted-foreground">Changes are saved locally to IndexedDB queue.</div>
|
|
98
|
+
</div>
|
|
99
|
+
{:else}
|
|
100
|
+
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-success/20 text-success">
|
|
101
|
+
<Wifi class="h-3.5 w-3.5" />
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<div class="font-semibold text-foreground">Online • Synced Connection</div>
|
|
105
|
+
<div class="text-[11px] text-muted-foreground">
|
|
106
|
+
{pendingMutations.length} pending mutation(s) in queue
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Actions -->
|
|
113
|
+
<div class="flex items-center gap-2">
|
|
114
|
+
{#if pendingMutations.length > 0}
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="text-[11px] text-muted-foreground hover:text-foreground underline cursor-pointer bg-transparent border-0"
|
|
118
|
+
onclick={() => { expanded = !expanded; }}
|
|
119
|
+
>
|
|
120
|
+
{expanded ? 'Hide Details' : `View Queue (${pendingMutations.length})`}
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
<Button
|
|
124
|
+
size="sm"
|
|
125
|
+
class="h-7 text-xs gap-1"
|
|
126
|
+
disabled={!isOnline || isSyncing}
|
|
127
|
+
onclick={triggerSync}
|
|
128
|
+
>
|
|
129
|
+
<RefreshCw class={cn('h-3 w-3', isSyncing ? 'animate-spin' : '')} />
|
|
130
|
+
{isSyncing ? 'Syncing...' : 'Sync Now'}
|
|
131
|
+
</Button>
|
|
132
|
+
{/if}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Queue List Drawer -->
|
|
137
|
+
{#if expanded && pendingMutations.length > 0}
|
|
138
|
+
<div class="mt-3 pt-3 border-t border-border/40 space-y-1.5 max-h-48 overflow-y-auto">
|
|
139
|
+
{#each pendingMutations as mut (mut.id)}
|
|
140
|
+
<div class="flex items-center justify-between p-2 rounded-md bg-card border border-border/60 text-[11px]">
|
|
141
|
+
<div class="flex items-center gap-2 truncate">
|
|
142
|
+
{#if mut.status === 'failed'}
|
|
143
|
+
<AlertCircle class="h-3.5 w-3.5 text-destructive shrink-0" />
|
|
144
|
+
{:else if mut.status === 'syncing'}
|
|
145
|
+
<RefreshCw class="h-3.5 w-3.5 text-primary animate-spin shrink-0" />
|
|
146
|
+
{:else if mut.status === 'synced'}
|
|
147
|
+
<CheckCircle2 class="h-3.5 w-3.5 text-success shrink-0" />
|
|
148
|
+
{:else}
|
|
149
|
+
<Clock class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
150
|
+
{/if}
|
|
151
|
+
|
|
152
|
+
<Badge variant="outline" class="font-mono text-[9px] uppercase px-1 py-0">{mut.action}</Badge>
|
|
153
|
+
<span class="font-medium text-foreground truncate">{mut.resource}</span>
|
|
154
|
+
<span class="text-muted-foreground font-mono text-[10px]">{mut.timestamp}</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{#if mut.status === 'failed' && onretry}
|
|
158
|
+
<Button
|
|
159
|
+
variant="ghost"
|
|
160
|
+
size="sm"
|
|
161
|
+
class="h-6 text-[10px] px-2 text-destructive hover:bg-destructive/10"
|
|
162
|
+
onclick={() => onretry?.(mut.id)}
|
|
163
|
+
>
|
|
164
|
+
Retry
|
|
165
|
+
</Button>
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
168
|
+
{/each}
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface PendingMutation {
|
|
2
|
+
id: string;
|
|
3
|
+
action: string;
|
|
4
|
+
resource: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
status?: 'pending' | 'syncing' | 'failed' | 'synced';
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
isOnline?: boolean;
|
|
11
|
+
pendingMutations?: PendingMutation[];
|
|
12
|
+
autoSync?: boolean;
|
|
13
|
+
onsync?: (mutations: PendingMutation[]) => void | Promise<void>;
|
|
14
|
+
onretry?: (mutationId: string) => void;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const OfflineSyncBanner: import("svelte").Component<Props, {}, "isOnline" | "pendingMutations">;
|
|
18
|
+
type OfflineSyncBanner = ReturnType<typeof OfflineSyncBanner>;
|
|
19
|
+
export default OfflineSyncBanner;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from './ui/button/index.js';
|
|
3
|
+
import { Badge } from './ui/badge/index.js';
|
|
4
|
+
import {
|
|
5
|
+
ChevronLeft,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
ZoomIn,
|
|
8
|
+
ZoomOut,
|
|
9
|
+
RotateCw,
|
|
10
|
+
Download,
|
|
11
|
+
Printer,
|
|
12
|
+
FileText,
|
|
13
|
+
Maximize2,
|
|
14
|
+
} from '@lucide/svelte';
|
|
15
|
+
import { cn } from '../utils.js';
|
|
16
|
+
|
|
17
|
+
export interface DocumentStamp {
|
|
18
|
+
id: string;
|
|
19
|
+
text: string;
|
|
20
|
+
page: number;
|
|
21
|
+
x: number; // percentage (0-100)
|
|
22
|
+
y: number; // percentage (0-100)
|
|
23
|
+
status?: 'approved' | 'rejected' | 'confidential' | 'reviewed';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
fileUrl?: string;
|
|
28
|
+
fileName?: string;
|
|
29
|
+
totalPages?: number;
|
|
30
|
+
currentPage?: number;
|
|
31
|
+
stamps?: DocumentStamp[];
|
|
32
|
+
onpagechange?: (page: number) => void;
|
|
33
|
+
ondownload?: () => void;
|
|
34
|
+
onprint?: () => void;
|
|
35
|
+
class?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
fileUrl = '',
|
|
40
|
+
fileName = 'Contract_Document.pdf',
|
|
41
|
+
totalPages = 1,
|
|
42
|
+
currentPage = $bindable(1),
|
|
43
|
+
stamps = [],
|
|
44
|
+
onpagechange,
|
|
45
|
+
ondownload,
|
|
46
|
+
onprint,
|
|
47
|
+
class: className = '',
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
let zoomLevel = $state(100);
|
|
51
|
+
let rotation = $state(0);
|
|
52
|
+
let isFullscreen = $state(false);
|
|
53
|
+
|
|
54
|
+
function prevPage() {
|
|
55
|
+
if (currentPage > 1) {
|
|
56
|
+
currentPage -= 1;
|
|
57
|
+
onpagechange?.(currentPage);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nextPage() {
|
|
62
|
+
if (currentPage < totalPages) {
|
|
63
|
+
currentPage += 1;
|
|
64
|
+
onpagechange?.(currentPage);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function zoomIn() {
|
|
69
|
+
zoomLevel = Math.min(zoomLevel + 25, 250);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function zoomOut() {
|
|
73
|
+
zoomLevel = Math.max(zoomLevel - 25, 50);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rotate() {
|
|
77
|
+
rotation = (rotation + 90) % 360;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleDownload() {
|
|
81
|
+
if (ondownload) {
|
|
82
|
+
ondownload();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (fileUrl) {
|
|
86
|
+
const a = document.createElement('a');
|
|
87
|
+
a.href = fileUrl;
|
|
88
|
+
a.download = fileName;
|
|
89
|
+
a.click();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handlePrint() {
|
|
94
|
+
if (onprint) {
|
|
95
|
+
onprint();
|
|
96
|
+
} else {
|
|
97
|
+
window.print();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pageStamps = $derived(
|
|
102
|
+
stamps.filter((s) => s.page === currentPage)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
function getStampVariant(status?: string) {
|
|
106
|
+
switch (status) {
|
|
107
|
+
case 'approved':
|
|
108
|
+
return 'bg-success/15 text-success border-success/30';
|
|
109
|
+
case 'rejected':
|
|
110
|
+
return 'bg-destructive/15 text-destructive border-destructive/30';
|
|
111
|
+
case 'confidential':
|
|
112
|
+
return 'bg-warning/15 text-warning border-warning/30';
|
|
113
|
+
default:
|
|
114
|
+
return 'bg-primary/15 text-primary border-primary/30';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
class={cn(
|
|
121
|
+
'flex flex-col rounded-xl border border-border bg-card shadow-xs text-xs overflow-hidden transition-all',
|
|
122
|
+
isFullscreen ? 'fixed inset-4 z-50 shadow-2xl' : 'min-h-[500px]',
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<!-- Toolbar Header -->
|
|
127
|
+
<div class="flex flex-wrap items-center justify-between gap-2 p-3 bg-muted/30 border-b border-border/60">
|
|
128
|
+
<!-- File Title -->
|
|
129
|
+
<div class="flex items-center gap-2 font-medium text-foreground truncate max-w-64">
|
|
130
|
+
<FileText class="h-4 w-4 text-primary shrink-0" />
|
|
131
|
+
<span class="truncate">{fileName}</span>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- Paging Controls -->
|
|
135
|
+
<div class="flex items-center gap-1.5">
|
|
136
|
+
<Button
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="sm"
|
|
139
|
+
class="h-7 w-7 p-0"
|
|
140
|
+
disabled={currentPage <= 1}
|
|
141
|
+
onclick={prevPage}
|
|
142
|
+
title="Previous Page"
|
|
143
|
+
>
|
|
144
|
+
<ChevronLeft class="h-4 w-4" />
|
|
145
|
+
</Button>
|
|
146
|
+
|
|
147
|
+
<div class="flex items-center gap-1 font-mono text-[11px] text-muted-foreground px-1">
|
|
148
|
+
<span class="font-semibold text-foreground">{currentPage}</span>
|
|
149
|
+
<span>/</span>
|
|
150
|
+
<span>{totalPages}</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<Button
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="sm"
|
|
156
|
+
class="h-7 w-7 p-0"
|
|
157
|
+
disabled={currentPage >= totalPages}
|
|
158
|
+
onclick={nextPage}
|
|
159
|
+
title="Next Page"
|
|
160
|
+
>
|
|
161
|
+
<ChevronRight class="h-4 w-4" />
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Zoom & Rotate Actions -->
|
|
166
|
+
<div class="flex items-center gap-1">
|
|
167
|
+
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" onclick={zoomOut} title="Zoom Out">
|
|
168
|
+
<ZoomOut class="h-3.5 w-3.5" />
|
|
169
|
+
</Button>
|
|
170
|
+
|
|
171
|
+
<span class="font-mono text-[11px] text-muted-foreground w-12 text-center">{zoomLevel}%</span>
|
|
172
|
+
|
|
173
|
+
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" onclick={zoomIn} title="Zoom In">
|
|
174
|
+
<ZoomIn class="h-3.5 w-3.5" />
|
|
175
|
+
</Button>
|
|
176
|
+
|
|
177
|
+
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" onclick={rotate} title="Rotate 90°">
|
|
178
|
+
<RotateCw class="h-3.5 w-3.5" />
|
|
179
|
+
</Button>
|
|
180
|
+
|
|
181
|
+
<Button
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="sm"
|
|
184
|
+
class="h-7 w-7 p-0"
|
|
185
|
+
onclick={() => { isFullscreen = !isFullscreen; }}
|
|
186
|
+
title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
|
187
|
+
>
|
|
188
|
+
<Maximize2 class="h-3.5 w-3.5" />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Export & Print -->
|
|
193
|
+
<div class="flex items-center gap-1.5">
|
|
194
|
+
<Button variant="outline" size="sm" class="h-7 text-xs gap-1" onclick={handlePrint}>
|
|
195
|
+
<Printer class="h-3.5 w-3.5" />
|
|
196
|
+
Print
|
|
197
|
+
</Button>
|
|
198
|
+
|
|
199
|
+
<Button size="sm" class="h-7 text-xs gap-1" onclick={handleDownload}>
|
|
200
|
+
<Download class="h-3.5 w-3.5" />
|
|
201
|
+
Download
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- Document Viewport -->
|
|
207
|
+
<div class="flex-1 overflow-auto bg-muted/40 p-6 flex items-center justify-center min-h-[400px]">
|
|
208
|
+
<div
|
|
209
|
+
class="relative bg-card rounded-lg shadow-md border border-border/80 transition-transform duration-200 overflow-hidden"
|
|
210
|
+
style={`transform: scale(${zoomLevel / 100}) rotate(${rotation}deg); transform-origin: center center; width: 595px; min-height: 842px;`}
|
|
211
|
+
>
|
|
212
|
+
{#if fileUrl}
|
|
213
|
+
<iframe
|
|
214
|
+
src={fileUrl}
|
|
215
|
+
title={fileName}
|
|
216
|
+
class="w-full h-[842px] border-0"
|
|
217
|
+
></iframe>
|
|
218
|
+
{:else}
|
|
219
|
+
<!-- Simulated Document Paper -->
|
|
220
|
+
<div class="p-12 space-y-6 text-foreground">
|
|
221
|
+
<div class="border-b border-border/60 pb-4 flex justify-between items-start">
|
|
222
|
+
<div>
|
|
223
|
+
<h2 class="text-base font-bold tracking-tight">{fileName.replace(/\.[^/.]+$/, '')}</h2>
|
|
224
|
+
<p class="text-[11px] text-muted-foreground">Document Sheet Reference • Page {currentPage}</p>
|
|
225
|
+
</div>
|
|
226
|
+
<Badge variant="outline" class="font-mono text-[10px]">P.{currentPage}</Badge>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div class="space-y-3 text-muted-foreground leading-relaxed text-xs">
|
|
230
|
+
<p>This is a rendered document preview surface. In production, provide a valid PDF / image URL via the <code>fileUrl</code> prop to stream direct binary pages.</p>
|
|
231
|
+
<p>The document supports electronic stamps, digital approval watermarks, interactive zooming, rotation and multi-page flipping.</p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="my-8 p-4 rounded-lg bg-muted/30 border border-border/60 space-y-2">
|
|
235
|
+
<div class="font-semibold text-foreground">Terms & Conditions of Execution</div>
|
|
236
|
+
<div class="text-[11px] text-muted-foreground">All parties acknowledge the contractual stipulations herein outlined and agreed upon via authenticated electronic signatures.</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
{/if}
|
|
240
|
+
|
|
241
|
+
<!-- Electronic Stamp Overlays -->
|
|
242
|
+
{#each pageStamps as stamp (stamp.id)}
|
|
243
|
+
<div
|
|
244
|
+
class={cn(
|
|
245
|
+
'absolute z-20 px-3 py-1.5 rounded-md border-2 font-bold text-xs uppercase tracking-wider shadow-sm rotate-[-12deg] pointer-events-none select-none',
|
|
246
|
+
getStampVariant(stamp.status)
|
|
247
|
+
)}
|
|
248
|
+
style={`left: ${stamp.x}%; top: ${stamp.y}%;`}
|
|
249
|
+
>
|
|
250
|
+
{stamp.text}
|
|
251
|
+
</div>
|
|
252
|
+
{/each}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DocumentStamp {
|
|
2
|
+
id: string;
|
|
3
|
+
text: string;
|
|
4
|
+
page: number;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
status?: 'approved' | 'rejected' | 'confidential' | 'reviewed';
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
fileUrl?: string;
|
|
11
|
+
fileName?: string;
|
|
12
|
+
totalPages?: number;
|
|
13
|
+
currentPage?: number;
|
|
14
|
+
stamps?: DocumentStamp[];
|
|
15
|
+
onpagechange?: (page: number) => void;
|
|
16
|
+
ondownload?: () => void;
|
|
17
|
+
onprint?: () => void;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const PdfDocumentViewer: import("svelte").Component<Props, {}, "currentPage">;
|
|
21
|
+
type PdfDocumentViewer = ReturnType<typeof PdfDocumentViewer>;
|
|
22
|
+
export default PdfDocumentViewer;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
data: Record<string, unknown>[];
|
|
8
|
+
rowField: string;
|
|
9
|
+
rowLabel?: string;
|
|
10
|
+
columnField: string;
|
|
11
|
+
columnLabel?: string;
|
|
12
|
+
valueField: string;
|
|
13
|
+
valueLabel?: string;
|
|
14
|
+
aggregator?: AggregationFn;
|
|
15
|
+
formatValue?: (val: number) => string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
data = [],
|
|
21
|
+
rowField,
|
|
22
|
+
rowLabel = rowField,
|
|
23
|
+
columnField,
|
|
24
|
+
columnLabel = columnField,
|
|
25
|
+
valueField,
|
|
26
|
+
valueLabel = valueField,
|
|
27
|
+
aggregator = 'sum',
|
|
28
|
+
formatValue = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 2 }),
|
|
29
|
+
class: className = '',
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const distinctRowValues = $derived(
|
|
33
|
+
Array.from(new Set(data.map((d) => String(d[rowField] ?? '—')))).sort()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const distinctColValues = $derived(
|
|
37
|
+
Array.from(new Set(data.map((d) => String(d[columnField] ?? '—')))).sort()
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function computeAgg(vals: number[]): number {
|
|
41
|
+
if (vals.length === 0) return 0;
|
|
42
|
+
if (aggregator === 'count') return vals.length;
|
|
43
|
+
if (aggregator === 'sum') return vals.reduce((a, b) => a + b, 0);
|
|
44
|
+
if (aggregator === 'avg') return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
45
|
+
if (aggregator === 'min') return Math.min(...vals);
|
|
46
|
+
if (aggregator === 'max') return Math.max(...vals);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pivotMatrix = $derived(() => {
|
|
51
|
+
const map = new Map<string, number[]>();
|
|
52
|
+
for (const item of data) {
|
|
53
|
+
const r = String(item[rowField] ?? '—');
|
|
54
|
+
const c = String(item[columnField] ?? '—');
|
|
55
|
+
const key = `${r}:::${c}`;
|
|
56
|
+
const num = Number(item[valueField]) || 0;
|
|
57
|
+
const existing = map.get(key);
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.push(num);
|
|
60
|
+
} else {
|
|
61
|
+
map.set(key, [num]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return map;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function getCellValue(r: string, c: string): number {
|
|
68
|
+
const key = `${r}:::${c}`;
|
|
69
|
+
const vals = pivotMatrix().get(key) ?? [];
|
|
70
|
+
return computeAgg(vals);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getRowTotal(r: string): number {
|
|
74
|
+
const vals = data
|
|
75
|
+
.filter((d) => String(d[rowField] ?? '—') === r)
|
|
76
|
+
.map((d) => Number(d[valueField]) || 0);
|
|
77
|
+
return computeAgg(vals);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getColTotal(c: string): number {
|
|
81
|
+
const vals = data
|
|
82
|
+
.filter((d) => String(d[columnField] ?? '—') === c)
|
|
83
|
+
.map((d) => Number(d[valueField]) || 0);
|
|
84
|
+
return computeAgg(vals);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const grandTotal = $derived(
|
|
88
|
+
computeAgg(data.map((d) => Number(d[valueField]) || 0))
|
|
89
|
+
);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
|
|
93
|
+
<div class="flex items-center justify-between gap-2 pb-2 border-b border-border/60">
|
|
94
|
+
<div class="font-semibold text-foreground">
|
|
95
|
+
Pivot Analysis <span class="text-muted-foreground font-normal">({rowLabel} × {columnLabel})</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="text-muted-foreground">
|
|
98
|
+
Aggregator: <strong class="text-primary uppercase">{aggregator}</strong> ({valueLabel})
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="overflow-x-auto rounded-lg border border-border/60">
|
|
103
|
+
<table class="w-full text-right border-collapse">
|
|
104
|
+
<thead class="bg-muted/40 font-semibold text-muted-foreground border-b border-border/60">
|
|
105
|
+
<tr>
|
|
106
|
+
<th class="px-3 py-2.5 text-left bg-muted/60">{rowLabel} \ {columnLabel}</th>
|
|
107
|
+
{#each distinctColValues as col (col)}
|
|
108
|
+
<th class="px-3 py-2.5 font-semibold text-foreground">{col}</th>
|
|
109
|
+
{/each}
|
|
110
|
+
<th class="px-3 py-2.5 bg-muted/60 font-semibold text-foreground">Total</th>
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody class="divide-y divide-border/40 font-mono">
|
|
114
|
+
{#each distinctRowValues as row (row)}
|
|
115
|
+
<tr class="hover:bg-muted/20 transition-colors">
|
|
116
|
+
<td class="px-3 py-2 text-left font-medium text-foreground bg-muted/20 font-sans">{row}</td>
|
|
117
|
+
{#each distinctColValues as col (col)}
|
|
118
|
+
{@const val = getCellValue(row, col)}
|
|
119
|
+
<td class={cn('px-3 py-2', val === 0 ? 'text-muted-foreground/40' : 'text-foreground')}>
|
|
120
|
+
{formatValue(val)}
|
|
121
|
+
</td>
|
|
122
|
+
{/each}
|
|
123
|
+
<td class="px-3 py-2 font-semibold text-foreground bg-muted/20">
|
|
124
|
+
{formatValue(getRowTotal(row))}
|
|
125
|
+
</td>
|
|
126
|
+
</tr>
|
|
127
|
+
{/each}
|
|
128
|
+
</tbody>
|
|
129
|
+
<tfoot class="bg-muted/60 font-semibold font-mono border-t-2 border-border text-foreground">
|
|
130
|
+
<tr>
|
|
131
|
+
<td class="px-3 py-2.5 text-left font-sans">Grand Total</td>
|
|
132
|
+
{#each distinctColValues as col (col)}
|
|
133
|
+
<td class="px-3 py-2.5">{formatValue(getColTotal(col))}</td>
|
|
134
|
+
{/each}
|
|
135
|
+
<td class="px-3 py-2.5 text-primary">{formatValue(grandTotal)}</td>
|
|
136
|
+
</tr>
|
|
137
|
+
</tfoot>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
|
|
2
|
+
interface Props {
|
|
3
|
+
data: Record<string, unknown>[];
|
|
4
|
+
rowField: string;
|
|
5
|
+
rowLabel?: string;
|
|
6
|
+
columnField: string;
|
|
7
|
+
columnLabel?: string;
|
|
8
|
+
valueField: string;
|
|
9
|
+
valueLabel?: string;
|
|
10
|
+
aggregator?: AggregationFn;
|
|
11
|
+
formatValue?: (val: number) => string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const PivotTable: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type PivotTable = ReturnType<typeof PivotTable>;
|
|
16
|
+
export default PivotTable;
|