@svadmin/ui 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ActivityFeed.svelte +135 -0
- package/dist/components/ActivityFeed.svelte.d.ts +21 -0
- package/dist/components/ApprovalActionCard.svelte +248 -0
- package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
- package/dist/components/AutoTable.svelte +8 -0
- package/dist/components/AutoTable.svelte.d.ts +7 -1
- package/dist/components/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
- package/dist/components/Cascader.svelte +205 -0
- package/dist/components/Cascader.svelte.d.ts +21 -0
- package/dist/components/ColumnHeaderFilter.svelte +178 -0
- package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
- package/dist/components/ColumnSettings.svelte +198 -0
- package/dist/components/ColumnSettings.svelte.d.ts +19 -0
- package/dist/components/DecisionTable.svelte +240 -0
- package/dist/components/DecisionTable.svelte.d.ts +22 -0
- package/dist/components/DraggableRowTable.svelte +117 -0
- package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
- package/dist/components/DrawerForm.svelte +110 -26
- package/dist/components/DrawerForm.svelte.d.ts +20 -8
- package/dist/components/DynamicFormList.svelte +214 -0
- package/dist/components/DynamicFormList.svelte.d.ts +62 -0
- package/dist/components/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -0
- package/dist/components/FieldRenderer.svelte +32 -0
- package/dist/components/FilterBuilder.svelte +310 -0
- package/dist/components/FilterBuilder.svelte.d.ts +23 -0
- package/dist/components/GanttChart.svelte +144 -0
- package/dist/components/GanttChart.svelte.d.ts +20 -0
- package/dist/components/ImageCropper.svelte +122 -0
- package/dist/components/ImageCropper.svelte.d.ts +12 -0
- package/dist/components/ImportWizard.svelte +336 -0
- package/dist/components/ImportWizard.svelte.d.ts +12 -0
- package/dist/components/JsonSchemaForm.svelte +149 -0
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
- package/dist/components/KanbanBoard.svelte +212 -0
- package/dist/components/KanbanBoard.svelte.d.ts +30 -0
- package/dist/components/MasterDetailView.svelte +134 -0
- package/dist/components/MasterDetailView.svelte.d.ts +16 -0
- package/dist/components/MediaLibraryModal.svelte +195 -0
- package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
- package/dist/components/MentionsInput.svelte +174 -0
- package/dist/components/MentionsInput.svelte.d.ts +21 -0
- package/dist/components/ModalForm.svelte +113 -28
- package/dist/components/ModalForm.svelte.d.ts +20 -8
- package/dist/components/MultiTabKeepAlive.svelte +143 -0
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
- package/dist/components/OfflineSyncBanner.svelte +172 -0
- package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
- package/dist/components/PdfDocumentViewer.svelte +255 -0
- package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
- package/dist/components/PivotTable.svelte +140 -0
- package/dist/components/PivotTable.svelte.d.ts +16 -0
- package/dist/components/PresenceAvatarGroup.svelte +89 -0
- package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
- package/dist/components/PrintableBill.svelte +149 -0
- package/dist/components/PrintableBill.svelte.d.ts +24 -0
- package/dist/components/SensitiveDataMask.svelte +125 -0
- package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
- package/dist/components/SignaturePad.svelte +264 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SplitPaneLayout.svelte +113 -0
- package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
- package/dist/components/SpreadsheetView.svelte +363 -0
- package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
- package/dist/components/StepForm.svelte +180 -0
- package/dist/components/StepForm.svelte.d.ts +23 -0
- package/dist/components/TableSummary.svelte +110 -0
- package/dist/components/TableSummary.svelte.d.ts +20 -0
- package/dist/components/Transfer.svelte +352 -0
- package/dist/components/Transfer.svelte.d.ts +28 -0
- package/dist/components/TreeSelect.svelte +307 -0
- package/dist/components/TreeSelect.svelte.d.ts +22 -0
- package/dist/components/TreeTable.svelte +237 -0
- package/dist/components/TreeTable.svelte.d.ts +28 -0
- package/dist/components/UnsavedChangesPrompt.svelte +44 -0
- package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
- package/dist/components/VersionDiffViewer.svelte +196 -0
- package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
- package/dist/components/VirtualTable.svelte +86 -54
- package/dist/components/VirtualTable.svelte.d.ts +14 -21
- package/dist/components/Watermark.svelte +166 -0
- package/dist/components/Watermark.svelte.d.ts +26 -0
- package/dist/components/ui/navigation-menu/index.d.ts +11 -0
- package/dist/components/ui/navigation-menu/index.js +13 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
- package/dist/index.d.ts +68 -3
- package/dist/index.js +42 -3
- package/package.json +2 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X, Pin, RefreshCw, XCircle } from '@lucide/svelte';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export interface WorkspaceTab {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
path: string;
|
|
10
|
+
closable?: boolean;
|
|
11
|
+
pinned?: boolean;
|
|
12
|
+
icon?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
tabs?: WorkspaceTab[];
|
|
17
|
+
activeTabId?: string;
|
|
18
|
+
onselect?: (tab: WorkspaceTab) => void;
|
|
19
|
+
onclose?: (tabId: string) => void;
|
|
20
|
+
oncloseothers?: (tabId: string) => void;
|
|
21
|
+
oncloseall?: () => void;
|
|
22
|
+
onrefresh?: (tab: WorkspaceTab) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
tabs = $bindable([]),
|
|
28
|
+
activeTabId = $bindable(''),
|
|
29
|
+
onselect,
|
|
30
|
+
onclose,
|
|
31
|
+
oncloseothers,
|
|
32
|
+
oncloseall,
|
|
33
|
+
onrefresh,
|
|
34
|
+
class: className = '',
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
function handleSelect(tab: WorkspaceTab) {
|
|
38
|
+
activeTabId = tab.id;
|
|
39
|
+
onselect?.(tab);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleClose(e: MouseEvent, tabId: string) {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
const idx = tabs.findIndex((t) => t.id === tabId);
|
|
45
|
+
if (idx === -1) return;
|
|
46
|
+
const isCurrentActive = activeTabId === tabId;
|
|
47
|
+
tabs = tabs.filter((t) => t.id !== tabId);
|
|
48
|
+
onclose?.(tabId);
|
|
49
|
+
|
|
50
|
+
if (isCurrentActive && tabs.length > 0) {
|
|
51
|
+
const nextTab = tabs[Math.min(idx, tabs.length - 1)];
|
|
52
|
+
if (nextTab) {
|
|
53
|
+
activeTabId = nextTab.id;
|
|
54
|
+
onselect?.(nextTab);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleCloseAll() {
|
|
60
|
+
const pinned = tabs.filter((t) => t.pinned);
|
|
61
|
+
tabs = pinned;
|
|
62
|
+
if (pinned.length > 0 && pinned[0]) {
|
|
63
|
+
activeTabId = pinned[0].id;
|
|
64
|
+
onselect?.(pinned[0]);
|
|
65
|
+
}
|
|
66
|
+
oncloseall?.();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleContextMenu(e: MouseEvent, tabId: string) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (oncloseothers) {
|
|
72
|
+
tabs = tabs.filter((t) => t.pinned || t.id === tabId);
|
|
73
|
+
activeTabId = tabId;
|
|
74
|
+
oncloseothers(tabId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<div class={cn('flex items-center justify-between border-b border-border/80 bg-muted/30 px-2 text-xs select-none', className)}>
|
|
80
|
+
<!-- Tabs list -->
|
|
81
|
+
<div class="flex items-center gap-1 overflow-x-auto py-1 scrollbar-none">
|
|
82
|
+
{#each tabs as tab (tab.id)}
|
|
83
|
+
{@const isActive = tab.id === activeTabId}
|
|
84
|
+
<div
|
|
85
|
+
role="button"
|
|
86
|
+
tabindex="0"
|
|
87
|
+
class={cn(
|
|
88
|
+
'group relative flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all cursor-pointer border',
|
|
89
|
+
isActive
|
|
90
|
+
? 'bg-card text-foreground border-border shadow-xs'
|
|
91
|
+
: 'bg-transparent text-muted-foreground border-transparent hover:bg-muted/60 hover:text-foreground'
|
|
92
|
+
)}
|
|
93
|
+
onclick={() => handleSelect(tab)}
|
|
94
|
+
onkeydown={(e) => { if (e.key === 'Enter') handleSelect(tab); }}
|
|
95
|
+
oncontextmenu={(e) => handleContextMenu(e, tab.id)}
|
|
96
|
+
>
|
|
97
|
+
{#if tab.pinned}
|
|
98
|
+
<Pin class="h-3 w-3 text-primary shrink-0 rotate-45" />
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<span class="truncate max-w-32">{tab.title}</span>
|
|
102
|
+
|
|
103
|
+
{#if tab.closable !== false && !tab.pinned}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="flex h-3.5 w-3.5 items-center justify-center rounded-full text-muted-foreground/60 hover:bg-destructive/10 hover:text-destructive transition-colors p-0 border-0 cursor-pointer"
|
|
107
|
+
onclick={(e) => handleClose(e, tab.id)}
|
|
108
|
+
>
|
|
109
|
+
<X class="h-2.5 w-2.5" />
|
|
110
|
+
</button>
|
|
111
|
+
{/if}
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Actions (Close All / Refresh) -->
|
|
117
|
+
<div class="flex items-center gap-1 pl-2 shrink-0">
|
|
118
|
+
{#if onrefresh}
|
|
119
|
+
{@const curTab = tabs.find((t) => t.id === activeTabId)}
|
|
120
|
+
{#if curTab}
|
|
121
|
+
<Button
|
|
122
|
+
variant="ghost"
|
|
123
|
+
size="sm"
|
|
124
|
+
class="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
|
125
|
+
onclick={() => onrefresh(curTab)}
|
|
126
|
+
>
|
|
127
|
+
<RefreshCw class="h-3 w-3" />
|
|
128
|
+
</Button>
|
|
129
|
+
{/if}
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
132
|
+
{#if tabs.some((t) => !t.pinned && t.closable !== false)}
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="sm"
|
|
136
|
+
class="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
|
137
|
+
onclick={handleCloseAll}
|
|
138
|
+
>
|
|
139
|
+
<XCircle class="h-3.5 w-3.5" />
|
|
140
|
+
</Button>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WorkspaceTab {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
path: string;
|
|
5
|
+
closable?: boolean;
|
|
6
|
+
pinned?: boolean;
|
|
7
|
+
icon?: string;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
tabs?: WorkspaceTab[];
|
|
11
|
+
activeTabId?: string;
|
|
12
|
+
onselect?: (tab: WorkspaceTab) => void;
|
|
13
|
+
onclose?: (tabId: string) => void;
|
|
14
|
+
oncloseothers?: (tabId: string) => void;
|
|
15
|
+
oncloseall?: () => void;
|
|
16
|
+
onrefresh?: (tab: WorkspaceTab) => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const MultiTabKeepAlive: import("svelte").Component<Props, {}, "tabs" | "activeTabId">;
|
|
20
|
+
type MultiTabKeepAlive = ReturnType<typeof MultiTabKeepAlive>;
|
|
21
|
+
export default MultiTabKeepAlive;
|
|
@@ -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;
|