@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,23 @@
|
|
|
1
|
+
import type { Snippet, Component } from 'svelte';
|
|
2
|
+
export interface FormStep {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
icon?: Component;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
steps: FormStep[];
|
|
9
|
+
currentStep?: number;
|
|
10
|
+
onstepchange?: (newStep: number, prevStep: number) => boolean | Promise<boolean> | undefined;
|
|
11
|
+
onfinish?: () => void | Promise<void>;
|
|
12
|
+
oncancel?: () => void;
|
|
13
|
+
submitText?: string;
|
|
14
|
+
nextText?: string;
|
|
15
|
+
prevText?: string;
|
|
16
|
+
isSubmitting?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
stepContent?: Snippet<[number]>;
|
|
20
|
+
}
|
|
21
|
+
declare const StepForm: Component<Props, {}, "currentStep">;
|
|
22
|
+
type StepForm = ReturnType<typeof StepForm>;
|
|
23
|
+
export default StepForm;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
export type AggregationType = 'sum' | 'avg' | 'count' | 'min' | 'max' | ((rows: Record<string, unknown>[]) => string | number);
|
|
5
|
+
|
|
6
|
+
export interface TableSummaryColumn {
|
|
7
|
+
key: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
align?: 'left' | 'center' | 'right';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
columns: TableSummaryColumn[];
|
|
14
|
+
data: Record<string, unknown>[];
|
|
15
|
+
aggregations: Record<string, AggregationType>;
|
|
16
|
+
title?: string;
|
|
17
|
+
titleColumnKey?: string;
|
|
18
|
+
prefix?: Record<string, string>;
|
|
19
|
+
suffix?: Record<string, string>;
|
|
20
|
+
precision?: Record<string, number>;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
columns,
|
|
26
|
+
data = [],
|
|
27
|
+
aggregations = {},
|
|
28
|
+
title = 'Total',
|
|
29
|
+
titleColumnKey,
|
|
30
|
+
prefix = {},
|
|
31
|
+
suffix = {},
|
|
32
|
+
precision = {},
|
|
33
|
+
class: className = '',
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const titleKey = $derived(titleColumnKey || (columns[0]?.key ?? ''));
|
|
37
|
+
|
|
38
|
+
function calculateAggregation(key: string, type: AggregationType): string | number {
|
|
39
|
+
if (!data.length) return '—';
|
|
40
|
+
|
|
41
|
+
if (typeof type === 'function') {
|
|
42
|
+
return type(data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (type === 'count') {
|
|
46
|
+
return data.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const numbers = data
|
|
50
|
+
.map((row) => Number(row[key]))
|
|
51
|
+
.filter((val) => !isNaN(val) && val !== null && val !== undefined);
|
|
52
|
+
|
|
53
|
+
if (numbers.length === 0) return '—';
|
|
54
|
+
|
|
55
|
+
const p = precision[key] !== undefined ? precision[key] : 2;
|
|
56
|
+
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'sum': {
|
|
59
|
+
const total = numbers.reduce((acc, curr) => acc + curr, 0);
|
|
60
|
+
return Number.isInteger(total) && p === 0 ? total : total.toFixed(p);
|
|
61
|
+
}
|
|
62
|
+
case 'avg': {
|
|
63
|
+
const avg = numbers.reduce((acc, curr) => acc + curr, 0) / numbers.length;
|
|
64
|
+
return avg.toFixed(p);
|
|
65
|
+
}
|
|
66
|
+
case 'min': {
|
|
67
|
+
const min = Math.min(...numbers);
|
|
68
|
+
return Number.isInteger(min) && p === 0 ? min : min.toFixed(p);
|
|
69
|
+
}
|
|
70
|
+
case 'max': {
|
|
71
|
+
const max = Math.max(...numbers);
|
|
72
|
+
return Number.isInteger(max) && p === 0 ? max : max.toFixed(p);
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
return '—';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div class={cn('w-full overflow-x-auto border-t-2 border-border/80 bg-muted/30 font-medium text-xs text-foreground', className)}>
|
|
81
|
+
<table class="w-full text-left border-collapse">
|
|
82
|
+
<tbody>
|
|
83
|
+
<tr class="h-10">
|
|
84
|
+
{#each columns as col (col.key)}
|
|
85
|
+
{@const isTitle = col.key === titleKey}
|
|
86
|
+
{@const aggType = aggregations[col.key]}
|
|
87
|
+
{@const align = col.align || 'left'}
|
|
88
|
+
<td
|
|
89
|
+
class={cn(
|
|
90
|
+
'px-4 py-2 tabular-nums',
|
|
91
|
+
align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left',
|
|
92
|
+
isTitle ? 'font-semibold text-foreground' : 'text-muted-foreground'
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{#if isTitle && !aggType}
|
|
96
|
+
<span>{title}</span>
|
|
97
|
+
{:else if aggType}
|
|
98
|
+
{@const result = calculateAggregation(col.key, aggType)}
|
|
99
|
+
<span class="font-semibold text-foreground">
|
|
100
|
+
{#if prefix[col.key]}{prefix[col.key]}{/if}{result}{#if suffix[col.key]}{suffix[col.key]}{/if}
|
|
101
|
+
</span>
|
|
102
|
+
{:else}
|
|
103
|
+
<span class="opacity-40">—</span>
|
|
104
|
+
{/if}
|
|
105
|
+
</td>
|
|
106
|
+
{/each}
|
|
107
|
+
</tr>
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AggregationType = 'sum' | 'avg' | 'count' | 'min' | 'max' | ((rows: Record<string, unknown>[]) => string | number);
|
|
2
|
+
export interface TableSummaryColumn {
|
|
3
|
+
key: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
align?: 'left' | 'center' | 'right';
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
columns: TableSummaryColumn[];
|
|
9
|
+
data: Record<string, unknown>[];
|
|
10
|
+
aggregations: Record<string, AggregationType>;
|
|
11
|
+
title?: string;
|
|
12
|
+
titleColumnKey?: string;
|
|
13
|
+
prefix?: Record<string, string>;
|
|
14
|
+
suffix?: Record<string, string>;
|
|
15
|
+
precision?: Record<string, number>;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const TableSummary: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type TableSummary = ReturnType<typeof TableSummary>;
|
|
20
|
+
export default TableSummary;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { ChevronRight, ChevronDown, Folder, File } from '@lucide/svelte';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Checkbox } from './ui/checkbox/index.js';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export interface TreeTableColumn {
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
width?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
data: Record<string, unknown>[];
|
|
16
|
+
columns: TreeTableColumn[];
|
|
17
|
+
primaryKey?: string;
|
|
18
|
+
childrenKey?: string;
|
|
19
|
+
selectable?: boolean;
|
|
20
|
+
selectedKeys?: (string | number)[];
|
|
21
|
+
onselect?: (selectedKeys: (string | number)[]) => void;
|
|
22
|
+
rowActions?: Snippet<[{ record: Record<string, unknown>; id: string | number }]>;
|
|
23
|
+
customCell?: Snippet<[{ column: TreeTableColumn; record: Record<string, unknown>; value: unknown }]>;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
data = [],
|
|
29
|
+
columns = [],
|
|
30
|
+
primaryKey = 'id',
|
|
31
|
+
childrenKey = 'children',
|
|
32
|
+
selectable = false,
|
|
33
|
+
selectedKeys = $bindable([]),
|
|
34
|
+
onselect,
|
|
35
|
+
rowActions,
|
|
36
|
+
customCell,
|
|
37
|
+
class: className = '',
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let expandedKeys = $state<Set<string | number>>(new Set());
|
|
41
|
+
|
|
42
|
+
interface FlatTreeItem {
|
|
43
|
+
record: Record<string, unknown>;
|
|
44
|
+
id: string | number;
|
|
45
|
+
level: number;
|
|
46
|
+
hasChildren: boolean;
|
|
47
|
+
isExpanded: boolean;
|
|
48
|
+
isLastChild?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function flattenTree(items: Record<string, unknown>[], level = 0): FlatTreeItem[] {
|
|
52
|
+
const flat: FlatTreeItem[] = [];
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const id = item[primaryKey] as string | number;
|
|
55
|
+
const children = item[childrenKey] as Record<string, unknown>[] | undefined;
|
|
56
|
+
const hasChildren = Array.isArray(children) && children.length > 0;
|
|
57
|
+
const isExpanded = expandedKeys.has(id);
|
|
58
|
+
|
|
59
|
+
flat.push({
|
|
60
|
+
record: item,
|
|
61
|
+
id,
|
|
62
|
+
level,
|
|
63
|
+
hasChildren,
|
|
64
|
+
isExpanded,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (hasChildren && isExpanded && children) {
|
|
68
|
+
flat.push(...flattenTree(children, level + 1));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return flat;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const flattenedRows = $derived(flattenTree(data));
|
|
75
|
+
|
|
76
|
+
function toggleExpand(id: string | number) {
|
|
77
|
+
const next = new Set(expandedKeys);
|
|
78
|
+
if (next.has(id)) {
|
|
79
|
+
next.delete(id);
|
|
80
|
+
} else {
|
|
81
|
+
next.add(id);
|
|
82
|
+
}
|
|
83
|
+
expandedKeys = next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expandAll() {
|
|
87
|
+
const all = new Set<string | number>();
|
|
88
|
+
function collect(items: Record<string, unknown>[]) {
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
const id = item[primaryKey] as string | number;
|
|
91
|
+
const children = item[childrenKey] as Record<string, unknown>[] | undefined;
|
|
92
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
93
|
+
all.add(id);
|
|
94
|
+
collect(children);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
collect(data);
|
|
99
|
+
expandedKeys = all;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collapseAll() {
|
|
103
|
+
expandedKeys = new Set();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAllDescendantIds(item: Record<string, unknown>): (string | number)[] {
|
|
107
|
+
const ids: (string | number)[] = [item[primaryKey] as string | number];
|
|
108
|
+
const children = item[childrenKey] as Record<string, unknown>[] | undefined;
|
|
109
|
+
if (Array.isArray(children)) {
|
|
110
|
+
for (const child of children) {
|
|
111
|
+
ids.push(...getAllDescendantIds(child));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return ids;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toggleSelect(item: Record<string, unknown>) {
|
|
118
|
+
const descendantIds = getAllDescendantIds(item);
|
|
119
|
+
const isSelected = selectedKeys.includes(item[primaryKey] as string | number);
|
|
120
|
+
|
|
121
|
+
let next: (string | number)[];
|
|
122
|
+
if (isSelected) {
|
|
123
|
+
next = selectedKeys.filter((k) => !descendantIds.includes(k));
|
|
124
|
+
} else {
|
|
125
|
+
next = Array.from(new Set([...selectedKeys, ...descendantIds]));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
selectedKeys = next;
|
|
129
|
+
onselect?.(next);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<div class={cn('w-full space-y-2', className)}>
|
|
134
|
+
<div class="flex items-center justify-between px-1 py-1 text-xs">
|
|
135
|
+
<div class="flex items-center gap-2">
|
|
136
|
+
<Button variant="ghost" size="sm" class="h-6 px-2 text-xs" onclick={expandAll}>
|
|
137
|
+
Expand All
|
|
138
|
+
</Button>
|
|
139
|
+
<Button variant="ghost" size="sm" class="h-6 px-2 text-xs text-muted-foreground" onclick={collapseAll}>
|
|
140
|
+
Collapse All
|
|
141
|
+
</Button>
|
|
142
|
+
</div>
|
|
143
|
+
{#if selectable && selectedKeys.length > 0}
|
|
144
|
+
<span class="text-xs text-muted-foreground">
|
|
145
|
+
Selected: <strong class="text-foreground">{selectedKeys.length}</strong>
|
|
146
|
+
</span>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="rounded-lg border border-border/80 overflow-hidden bg-card">
|
|
151
|
+
<table class="w-full text-left text-xs border-collapse">
|
|
152
|
+
<thead class="bg-muted/50 border-b border-border/60 font-semibold text-muted-foreground">
|
|
153
|
+
<tr>
|
|
154
|
+
{#if selectable}
|
|
155
|
+
<th class="w-8 px-3 py-2 text-center"></th>
|
|
156
|
+
{/if}
|
|
157
|
+
{#each columns as col (col.key)}
|
|
158
|
+
<th class="px-3 py-2" style={col.width ? `width: ${col.width}` : ''}>
|
|
159
|
+
{col.label}
|
|
160
|
+
</th>
|
|
161
|
+
{/each}
|
|
162
|
+
{#if rowActions}
|
|
163
|
+
<th class="w-20 px-3 py-2 text-right">Actions</th>
|
|
164
|
+
{/if}
|
|
165
|
+
</tr>
|
|
166
|
+
</thead>
|
|
167
|
+
<tbody class="divide-y divide-border/40">
|
|
168
|
+
{#each flattenedRows as row (row.id)}
|
|
169
|
+
{@const isSelected = selectedKeys.includes(row.id)}
|
|
170
|
+
<tr class="hover:bg-muted/30 transition-colors {isSelected ? 'bg-primary/5' : ''}">
|
|
171
|
+
{#if selectable}
|
|
172
|
+
<td class="w-8 px-3 py-2 text-center">
|
|
173
|
+
<Checkbox
|
|
174
|
+
checked={isSelected}
|
|
175
|
+
onCheckedChange={() => toggleSelect(row.record)}
|
|
176
|
+
aria-label="Select {row.id}"
|
|
177
|
+
/>
|
|
178
|
+
</td>
|
|
179
|
+
{/if}
|
|
180
|
+
|
|
181
|
+
{#each columns as col, colIdx (col.key)}
|
|
182
|
+
{@const val = row.record[col.key]}
|
|
183
|
+
<td class="px-3 py-2">
|
|
184
|
+
{#if colIdx === 0}
|
|
185
|
+
<!-- Primary tree node column with indent -->
|
|
186
|
+
<div
|
|
187
|
+
class="flex items-center gap-1.5"
|
|
188
|
+
style="padding-left: {row.level * 20}px;"
|
|
189
|
+
>
|
|
190
|
+
{#if row.hasChildren}
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
class="h-5 w-5 flex items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
194
|
+
onclick={() => toggleExpand(row.id)}
|
|
195
|
+
aria-label={row.isExpanded ? 'Collapse' : 'Expand'}
|
|
196
|
+
>
|
|
197
|
+
{#if row.isExpanded}
|
|
198
|
+
<ChevronDown class="h-3.5 w-3.5" />
|
|
199
|
+
{:else}
|
|
200
|
+
<ChevronRight class="h-3.5 w-3.5" />
|
|
201
|
+
{/if}
|
|
202
|
+
</button>
|
|
203
|
+
<Folder class="h-3.5 w-3.5 text-primary/70 shrink-0" />
|
|
204
|
+
{:else}
|
|
205
|
+
<span class="w-5 inline-block"></span>
|
|
206
|
+
<File class="h-3.5 w-3.5 text-muted-foreground/50 shrink-0" />
|
|
207
|
+
{/if}
|
|
208
|
+
|
|
209
|
+
<span class="font-medium text-foreground truncate">
|
|
210
|
+
{val ?? '—'}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
{:else if customCell}
|
|
214
|
+
{@render customCell({ column: col, record: row.record, value: val })}
|
|
215
|
+
{:else}
|
|
216
|
+
<span class="text-foreground truncate">{val ?? '—'}</span>
|
|
217
|
+
{/if}
|
|
218
|
+
</td>
|
|
219
|
+
{/each}
|
|
220
|
+
|
|
221
|
+
{#if rowActions}
|
|
222
|
+
<td class="w-20 px-3 py-2 text-right">
|
|
223
|
+
{@render rowActions({ record: row.record, id: row.id })}
|
|
224
|
+
</td>
|
|
225
|
+
{/if}
|
|
226
|
+
</tr>
|
|
227
|
+
{:else}
|
|
228
|
+
<tr>
|
|
229
|
+
<td colspan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} class="py-8 text-center text-muted-foreground">
|
|
230
|
+
No data available
|
|
231
|
+
</td>
|
|
232
|
+
</tr>
|
|
233
|
+
{/each}
|
|
234
|
+
</tbody>
|
|
235
|
+
</table>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface TreeTableColumn {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
width?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
data: Record<string, unknown>[];
|
|
9
|
+
columns: TreeTableColumn[];
|
|
10
|
+
primaryKey?: string;
|
|
11
|
+
childrenKey?: string;
|
|
12
|
+
selectable?: boolean;
|
|
13
|
+
selectedKeys?: (string | number)[];
|
|
14
|
+
onselect?: (selectedKeys: (string | number)[]) => void;
|
|
15
|
+
rowActions?: Snippet<[{
|
|
16
|
+
record: Record<string, unknown>;
|
|
17
|
+
id: string | number;
|
|
18
|
+
}]>;
|
|
19
|
+
customCell?: Snippet<[{
|
|
20
|
+
column: TreeTableColumn;
|
|
21
|
+
record: Record<string, unknown>;
|
|
22
|
+
value: unknown;
|
|
23
|
+
}]>;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const TreeTable: import("svelte").Component<Props, {}, "selectedKeys">;
|
|
27
|
+
type TreeTable = ReturnType<typeof TreeTable>;
|
|
28
|
+
export default TreeTable;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
dirty: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { dirty, message }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const i18n = useTranslation();
|
|
13
|
+
const promptMessage = $derived(
|
|
14
|
+
message ?? i18n.t('common.warnUnsavedChanges', { defaultValue: 'You have unsaved changes. Are you sure you want to leave?' })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
|
18
|
+
if (dirty) {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
e.returnValue = promptMessage;
|
|
21
|
+
return promptMessage;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (typeof window === 'undefined') return;
|
|
27
|
+
|
|
28
|
+
if (dirty) {
|
|
29
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
30
|
+
} else {
|
|
31
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onDestroy(() => {
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { Badge } from './ui/badge/index.js';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Columns2, AlignJustify } from '@lucide/svelte';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
oldValue: Record<string, unknown>;
|
|
10
|
+
newValue: Record<string, unknown>;
|
|
11
|
+
oldTitle?: string;
|
|
12
|
+
newTitle?: string;
|
|
13
|
+
viewMode?: 'split' | 'unified';
|
|
14
|
+
fieldLabels?: Record<string, string>;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
oldValue = {},
|
|
20
|
+
newValue = {},
|
|
21
|
+
oldTitle = 'Previous Version',
|
|
22
|
+
newTitle = 'Current Version',
|
|
23
|
+
viewMode = 'split',
|
|
24
|
+
fieldLabels = {},
|
|
25
|
+
class: className = '',
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let mode = $state<'split' | 'unified'>(untrack(() => viewMode));
|
|
29
|
+
|
|
30
|
+
interface FieldDiff {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
oldVal: unknown;
|
|
34
|
+
newVal: unknown;
|
|
35
|
+
status: 'added' | 'removed' | 'modified' | 'unchanged';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const diffList = $derived.by<FieldDiff[]>(() => {
|
|
39
|
+
const oldObj = oldValue || {};
|
|
40
|
+
const newObj = newValue || {};
|
|
41
|
+
const allKeys = Array.from(new Set([...Object.keys(oldObj), ...Object.keys(newObj)]));
|
|
42
|
+
|
|
43
|
+
return allKeys.map((key) => {
|
|
44
|
+
const hasOld = key in oldObj && oldObj[key] !== undefined;
|
|
45
|
+
const hasNew = key in newObj && newObj[key] !== undefined;
|
|
46
|
+
const oldVal = oldObj[key];
|
|
47
|
+
const newVal = newObj[key];
|
|
48
|
+
|
|
49
|
+
let status: FieldDiff['status'] = 'unchanged';
|
|
50
|
+
if (!hasOld && hasNew) {
|
|
51
|
+
status = 'added';
|
|
52
|
+
} else if (hasOld && !hasNew) {
|
|
53
|
+
status = 'removed';
|
|
54
|
+
} else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
55
|
+
status = 'modified';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
key,
|
|
60
|
+
label: fieldLabels[key] || key,
|
|
61
|
+
oldVal,
|
|
62
|
+
newVal,
|
|
63
|
+
status,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const modifiedCount = $derived(diffList.filter((d) => d.status !== 'unchanged').length);
|
|
69
|
+
|
|
70
|
+
function formatValue(val: unknown): string {
|
|
71
|
+
if (val === undefined || val === null) return '—';
|
|
72
|
+
if (typeof val === 'object') {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.stringify(val, null, 2);
|
|
75
|
+
} catch {
|
|
76
|
+
return String(val);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return String(val);
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class={cn('rounded-xl border border-border bg-card shadow-xs overflow-hidden', className)}>
|
|
84
|
+
<!-- Header & Toolbar -->
|
|
85
|
+
<div class="flex items-center justify-between p-3.5 border-b border-border/60 bg-muted/20">
|
|
86
|
+
<div class="flex items-center gap-2">
|
|
87
|
+
<h4 class="text-xs font-semibold text-foreground">Record Comparison</h4>
|
|
88
|
+
<Badge variant="outline" class="text-[11px]">
|
|
89
|
+
{modifiedCount} change{modifiedCount === 1 ? '' : 's'}
|
|
90
|
+
</Badge>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="flex items-center gap-1">
|
|
94
|
+
<Button
|
|
95
|
+
variant={mode === 'split' ? 'secondary' : 'ghost'}
|
|
96
|
+
size="sm"
|
|
97
|
+
class="h-7 text-xs gap-1 px-2"
|
|
98
|
+
onclick={() => { mode = 'split'; }}
|
|
99
|
+
>
|
|
100
|
+
<Columns2 class="h-3.5 w-3.5" />
|
|
101
|
+
Side by Side
|
|
102
|
+
</Button>
|
|
103
|
+
<Button
|
|
104
|
+
variant={mode === 'unified' ? 'secondary' : 'ghost'}
|
|
105
|
+
size="sm"
|
|
106
|
+
class="h-7 text-xs gap-1 px-2"
|
|
107
|
+
onclick={() => { mode = 'unified'; }}
|
|
108
|
+
>
|
|
109
|
+
<AlignJustify class="h-3.5 w-3.5" />
|
|
110
|
+
Unified
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{#if mode === 'split'}
|
|
116
|
+
<!-- Split Side-by-Side View -->
|
|
117
|
+
<div class="grid grid-cols-2 divide-x divide-border/60 text-xs">
|
|
118
|
+
<div class="p-3 bg-muted/10 font-semibold text-muted-foreground border-b border-border/60">{oldTitle}</div>
|
|
119
|
+
<div class="p-3 bg-muted/10 font-semibold text-muted-foreground border-b border-border/60">{newTitle}</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="divide-y divide-border/40 text-xs">
|
|
123
|
+
{#each diffList as diff (diff.key)}
|
|
124
|
+
{@const isDiff = diff.status !== 'unchanged'}
|
|
125
|
+
<div class={cn('grid grid-cols-2 divide-x divide-border/40 transition-colors', isDiff ? 'bg-muted/15' : '')}>
|
|
126
|
+
<!-- Left Column -->
|
|
127
|
+
<div class={cn('p-3 space-y-1', diff.status === 'removed' ? 'bg-destructive/5' : '')}>
|
|
128
|
+
<div class="flex items-center justify-between text-[11px] text-muted-foreground font-medium">
|
|
129
|
+
<span>{diff.label}</span>
|
|
130
|
+
{#if diff.status === 'removed'}
|
|
131
|
+
<Badge variant="outline" class="bg-destructive/15 text-destructive border-destructive/20 text-[10px]">Deleted</Badge>
|
|
132
|
+
{/if}
|
|
133
|
+
</div>
|
|
134
|
+
<div class={cn('font-mono text-xs break-words', diff.status === 'removed' ? 'line-through text-destructive' : 'text-foreground')}>
|
|
135
|
+
{formatValue(diff.oldVal)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Right Column -->
|
|
140
|
+
<div class={cn('p-3 space-y-1', diff.status === 'added' ? 'bg-success/5' : diff.status === 'modified' ? 'bg-warning/5' : '')}>
|
|
141
|
+
<div class="flex items-center justify-between text-[11px] text-muted-foreground font-medium">
|
|
142
|
+
<span>{diff.label}</span>
|
|
143
|
+
{#if diff.status === 'added'}
|
|
144
|
+
<Badge variant="outline" class="bg-success/15 text-success border-success/20 text-[10px]">Added</Badge>
|
|
145
|
+
{:else if diff.status === 'modified'}
|
|
146
|
+
<Badge variant="outline" class="bg-warning/15 text-warning-foreground border-warning/20 text-[10px]">Modified</Badge>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
<div class={cn('font-mono text-xs break-words', diff.status === 'added' ? 'text-success font-medium' : diff.status === 'modified' ? 'text-warning-foreground font-medium' : 'text-foreground')}>
|
|
150
|
+
{formatValue(diff.newVal)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
{:else}
|
|
157
|
+
<!-- Unified List View -->
|
|
158
|
+
<div class="divide-y divide-border/40 text-xs">
|
|
159
|
+
{#each diffList as diff (diff.key)}
|
|
160
|
+
<div class="p-3 space-y-2">
|
|
161
|
+
<div class="flex items-center justify-between">
|
|
162
|
+
<span class="font-medium text-foreground">{diff.label}</span>
|
|
163
|
+
{#if diff.status === 'added'}
|
|
164
|
+
<Badge variant="outline" class="bg-success/15 text-success border-success/20 text-[10px]">Added</Badge>
|
|
165
|
+
{:else if diff.status === 'removed'}
|
|
166
|
+
<Badge variant="outline" class="bg-destructive/15 text-destructive border-destructive/20 text-[10px]">Removed</Badge>
|
|
167
|
+
{:else if diff.status === 'modified'}
|
|
168
|
+
<Badge variant="outline" class="bg-warning/15 text-warning-foreground border-warning/20 text-[10px]">Modified</Badge>
|
|
169
|
+
{:else}
|
|
170
|
+
<Badge variant="outline" class="text-[10px] text-muted-foreground">Unchanged</Badge>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{#if diff.status === 'modified'}
|
|
175
|
+
<div class="space-y-1 rounded-md bg-muted/40 p-2 font-mono text-[11px]">
|
|
176
|
+
<div class="text-destructive line-through">- {formatValue(diff.oldVal)}</div>
|
|
177
|
+
<div class="text-success font-medium">+ {formatValue(diff.newVal)}</div>
|
|
178
|
+
</div>
|
|
179
|
+
{:else if diff.status === 'added'}
|
|
180
|
+
<div class="rounded-md bg-success/10 p-2 font-mono text-[11px] text-success">
|
|
181
|
+
+ {formatValue(diff.newVal)}
|
|
182
|
+
</div>
|
|
183
|
+
{:else if diff.status === 'removed'}
|
|
184
|
+
<div class="rounded-md bg-destructive/10 p-2 font-mono text-[11px] text-destructive line-through">
|
|
185
|
+
- {formatValue(diff.oldVal)}
|
|
186
|
+
</div>
|
|
187
|
+
{:else}
|
|
188
|
+
<div class="font-mono text-[11px] text-muted-foreground">
|
|
189
|
+
{formatValue(diff.newVal)}
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
192
|
+
</div>
|
|
193
|
+
{/each}
|
|
194
|
+
</div>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
oldValue: Record<string, unknown>;
|
|
3
|
+
newValue: Record<string, unknown>;
|
|
4
|
+
oldTitle?: string;
|
|
5
|
+
newTitle?: string;
|
|
6
|
+
viewMode?: 'split' | 'unified';
|
|
7
|
+
fieldLabels?: Record<string, string>;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const VersionDiffViewer: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type VersionDiffViewer = ReturnType<typeof VersionDiffViewer>;
|
|
12
|
+
export default VersionDiffViewer;
|