@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.
Files changed (104) 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/AutoTable.svelte +8 -0
  6. package/dist/components/AutoTable.svelte.d.ts +7 -1
  7. package/dist/components/CanvasAnnotation.svelte +389 -0
  8. package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
  9. package/dist/components/Cascader.svelte +205 -0
  10. package/dist/components/Cascader.svelte.d.ts +21 -0
  11. package/dist/components/ColumnHeaderFilter.svelte +178 -0
  12. package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
  13. package/dist/components/ColumnSettings.svelte +198 -0
  14. package/dist/components/ColumnSettings.svelte.d.ts +19 -0
  15. package/dist/components/DecisionTable.svelte +240 -0
  16. package/dist/components/DecisionTable.svelte.d.ts +22 -0
  17. package/dist/components/DraggableRowTable.svelte +117 -0
  18. package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
  19. package/dist/components/DrawerForm.svelte +110 -26
  20. package/dist/components/DrawerForm.svelte.d.ts +20 -8
  21. package/dist/components/DynamicFormList.svelte +214 -0
  22. package/dist/components/DynamicFormList.svelte.d.ts +62 -0
  23. package/dist/components/EditableTable.svelte +195 -0
  24. package/dist/components/EditableTable.svelte.d.ts +25 -0
  25. package/dist/components/FieldRenderer.svelte +32 -0
  26. package/dist/components/FilterBuilder.svelte +310 -0
  27. package/dist/components/FilterBuilder.svelte.d.ts +23 -0
  28. package/dist/components/GanttChart.svelte +144 -0
  29. package/dist/components/GanttChart.svelte.d.ts +20 -0
  30. package/dist/components/ImageCropper.svelte +122 -0
  31. package/dist/components/ImageCropper.svelte.d.ts +12 -0
  32. package/dist/components/ImportWizard.svelte +336 -0
  33. package/dist/components/ImportWizard.svelte.d.ts +12 -0
  34. package/dist/components/JsonSchemaForm.svelte +149 -0
  35. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
  36. package/dist/components/KanbanBoard.svelte +212 -0
  37. package/dist/components/KanbanBoard.svelte.d.ts +30 -0
  38. package/dist/components/MasterDetailView.svelte +134 -0
  39. package/dist/components/MasterDetailView.svelte.d.ts +16 -0
  40. package/dist/components/MediaLibraryModal.svelte +195 -0
  41. package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
  42. package/dist/components/MentionsInput.svelte +174 -0
  43. package/dist/components/MentionsInput.svelte.d.ts +21 -0
  44. package/dist/components/ModalForm.svelte +113 -28
  45. package/dist/components/ModalForm.svelte.d.ts +20 -8
  46. package/dist/components/MultiTabKeepAlive.svelte +143 -0
  47. package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
  48. package/dist/components/OfflineSyncBanner.svelte +172 -0
  49. package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
  50. package/dist/components/PdfDocumentViewer.svelte +255 -0
  51. package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
  52. package/dist/components/PivotTable.svelte +140 -0
  53. package/dist/components/PivotTable.svelte.d.ts +16 -0
  54. package/dist/components/PresenceAvatarGroup.svelte +89 -0
  55. package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
  56. package/dist/components/PrintableBill.svelte +149 -0
  57. package/dist/components/PrintableBill.svelte.d.ts +24 -0
  58. package/dist/components/SensitiveDataMask.svelte +125 -0
  59. package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
  60. package/dist/components/SignaturePad.svelte +264 -0
  61. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  62. package/dist/components/SplitPaneLayout.svelte +113 -0
  63. package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
  64. package/dist/components/SpreadsheetView.svelte +363 -0
  65. package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
  66. package/dist/components/StepForm.svelte +180 -0
  67. package/dist/components/StepForm.svelte.d.ts +23 -0
  68. package/dist/components/TableSummary.svelte +110 -0
  69. package/dist/components/TableSummary.svelte.d.ts +20 -0
  70. package/dist/components/Transfer.svelte +352 -0
  71. package/dist/components/Transfer.svelte.d.ts +28 -0
  72. package/dist/components/TreeSelect.svelte +307 -0
  73. package/dist/components/TreeSelect.svelte.d.ts +22 -0
  74. package/dist/components/TreeTable.svelte +237 -0
  75. package/dist/components/TreeTable.svelte.d.ts +28 -0
  76. package/dist/components/UnsavedChangesPrompt.svelte +44 -0
  77. package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
  78. package/dist/components/VersionDiffViewer.svelte +196 -0
  79. package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
  80. package/dist/components/VirtualTable.svelte +86 -54
  81. package/dist/components/VirtualTable.svelte.d.ts +14 -21
  82. package/dist/components/Watermark.svelte +166 -0
  83. package/dist/components/Watermark.svelte.d.ts +26 -0
  84. package/dist/components/ui/navigation-menu/index.d.ts +11 -0
  85. package/dist/components/ui/navigation-menu/index.js +13 -0
  86. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
  87. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
  88. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
  89. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
  90. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
  91. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
  92. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
  93. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
  94. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
  95. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
  96. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
  97. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
  98. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
  99. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
  100. package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
  101. package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
  102. package/dist/index.d.ts +68 -3
  103. package/dist/index.js +42 -3
  104. package/package.json +2 -2
@@ -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;
@@ -1,74 +1,106 @@
1
1
  <script lang="ts">
2
- /**
3
- * VirtualTable — A lightweight virtual scrolling wrapper for large datasets.
4
- * Uses IntersectionObserver-based approach instead of a heavy dependency.
5
- *
6
- * Usage:
7
- * <VirtualTable items={rows} rowHeight={40} containerHeight={500}>
8
- * {#snippet row(item, index)}
9
- * <tr>...</tr>
10
- * {/snippet}
11
- * </VirtualTable>
12
- */
13
2
  import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils.js';
14
4
 
15
- interface Props<T> {
16
- items: T[];
17
- /** Estimated row height in pixels */
18
- rowHeight?: number;
19
- /** Container height in pixels */
20
- containerHeight?: number;
21
- /** Number of extra rows to render above/below visible area */
5
+ export interface VirtualTableColumn {
6
+ key: string;
7
+ label: string;
8
+ width?: string;
9
+ align?: 'left' | 'center' | 'right';
10
+ }
11
+
12
+ interface Props {
13
+ items?: Record<string, unknown>[];
14
+ columns: VirtualTableColumn[];
15
+ itemHeight?: number;
16
+ height?: number;
22
17
  overscan?: number;
23
- /** Row rendering snippet */
24
- row: Snippet<[T, number]>;
18
+ rowKey?: string;
25
19
  class?: string;
20
+ rowSnippet?: Snippet<[Record<string, unknown>, number]>;
26
21
  }
27
22
 
28
23
  let {
29
- items,
30
- rowHeight = 40,
31
- containerHeight = 500,
32
- overscan = 5,
33
- row,
24
+ items = [],
25
+ columns,
26
+ itemHeight = 44,
27
+ height = 360,
28
+ overscan = 4,
29
+ rowKey = 'id',
34
30
  class: className = '',
35
- }: Props<unknown> = $props();
31
+ rowSnippet,
32
+ }: Props = $props();
36
33
 
37
34
  let scrollTop = $state(0);
38
35
 
39
- const visibleRange = $derived.by(() => {
40
- const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
41
- const visibleCount = Math.ceil(containerHeight / rowHeight);
42
- const end = Math.min(items.length - 1, start + visibleCount + overscan * 2);
43
- return { start, end };
44
- });
36
+ function handleScroll(e: Event) {
37
+ const target = e.currentTarget as HTMLElement;
38
+ scrollTop = target.scrollTop;
39
+ }
45
40
 
46
- const visibleItems = $derived(
47
- items.slice(visibleRange.start, visibleRange.end + 1).map((item, i) => ({
41
+ const totalHeight = $derived(items.length * itemHeight);
42
+ const startIndex = $derived(Math.max(0, Math.floor(scrollTop / itemHeight) - overscan));
43
+ const endIndex = $derived(Math.min(items.length, Math.ceil((scrollTop + height) / itemHeight) + overscan));
44
+ const visibleSlice = $derived(
45
+ items.slice(startIndex, endIndex).map((item, idx) => ({
48
46
  item,
49
- index: visibleRange.start + i,
50
- offsetY: (visibleRange.start + i) * rowHeight,
47
+ index: startIndex + idx,
48
+ top: (startIndex + idx) * itemHeight,
51
49
  }))
52
50
  );
53
-
54
- function handleScroll(e: Event) {
55
- const target = e.target as HTMLDivElement;
56
- scrollTop = target.scrollTop;
57
- }
58
51
  </script>
59
52
 
60
- <div
61
- class="overflow-auto {className}"
62
- style="height: {containerHeight}px;"
63
- onscroll={handleScroll}
64
- >
65
- <table class="w-full relative" style="border-spacing: 0;">
66
- <tbody style="display: block;">
67
- <tr style="height: {visibleRange.start * rowHeight}px; border: none; padding: 0; display: block;"></tr>
68
- {#each visibleItems as { item, index } (index)}
69
- {@render row(item, index)}
53
+ <div class={cn('w-full rounded-xl border border-border bg-card shadow-xs overflow-hidden flex flex-col text-xs', className)}>
54
+ <!-- Sticky Header -->
55
+ <div class="border-b border-border/80 bg-muted/40 font-semibold text-muted-foreground flex items-center h-10 px-4 select-none shrink-0">
56
+ {#each columns as col (col.key)}
57
+ <div
58
+ style={col.width ? `width: ${col.width}; flex-shrink: 0;` : 'flex: 1; min-width: 0;'}
59
+ class={cn('truncate px-2', col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left')}
60
+ >
61
+ {col.label}
62
+ </div>
63
+ {/each}
64
+ </div>
65
+
66
+ <!-- Virtual Scroll Viewport -->
67
+ <div
68
+ class="overflow-y-auto relative w-full"
69
+ style="height: {height}px;"
70
+ onscroll={handleScroll}
71
+ >
72
+ <div style="height: {totalHeight}px; width: 100%; position: relative;">
73
+ {#each visibleSlice as { item, index, top } (item[rowKey] ?? index)}
74
+ <div
75
+ class="absolute left-0 right-0 flex items-center px-4 border-b border-border/40 hover:bg-muted/30 transition-colors"
76
+ style="top: {top}px; height: {itemHeight}px;"
77
+ >
78
+ {#if rowSnippet}
79
+ {@render rowSnippet(item, index)}
80
+ {:else}
81
+ {#each columns as col (col.key)}
82
+ <div
83
+ style={col.width ? `width: ${col.width}; flex-shrink: 0;` : 'flex: 1; min-width: 0;'}
84
+ class={cn('truncate px-2 font-medium text-foreground', col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left')}
85
+ >
86
+ {item[col.key] ?? '—'}
87
+ </div>
88
+ {/each}
89
+ {/if}
90
+ </div>
70
91
  {/each}
71
- <tr style="height: {Math.max(0, (items.length - visibleRange.end - 1) * rowHeight)}px; border: none; padding: 0; display: block;"></tr>
72
- </tbody>
73
- </table>
92
+
93
+ {#if items.length === 0}
94
+ <div class="h-32 flex items-center justify-center text-muted-foreground text-xs">
95
+ No records to display
96
+ </div>
97
+ {/if}
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Footer Info -->
102
+ <div class="flex items-center justify-between px-4 py-2 border-t border-border/60 bg-muted/20 text-[11px] text-muted-foreground shrink-0">
103
+ <span>Total <strong class="text-foreground font-semibold">{items.length}</strong> rows</span>
104
+ <span>Rendered {visibleSlice.length} in viewport</span>
105
+ </div>
74
106
  </div>
@@ -1,27 +1,20 @@
1
- /**
2
- * VirtualTable — A lightweight virtual scrolling wrapper for large datasets.
3
- * Uses IntersectionObserver-based approach instead of a heavy dependency.
4
- *
5
- * Usage:
6
- * <VirtualTable items={rows} rowHeight={40} containerHeight={500}>
7
- * {#snippet row(item, index)}
8
- * <tr>...</tr>
9
- * {/snippet}
10
- * </VirtualTable>
11
- */
12
1
  import type { Snippet } from 'svelte';
13
- interface Props<T> {
14
- items: T[];
15
- /** Estimated row height in pixels */
16
- rowHeight?: number;
17
- /** Container height in pixels */
18
- containerHeight?: number;
19
- /** Number of extra rows to render above/below visible area */
2
+ export interface VirtualTableColumn {
3
+ key: string;
4
+ label: string;
5
+ width?: string;
6
+ align?: 'left' | 'center' | 'right';
7
+ }
8
+ interface Props {
9
+ items?: Record<string, unknown>[];
10
+ columns: VirtualTableColumn[];
11
+ itemHeight?: number;
12
+ height?: number;
20
13
  overscan?: number;
21
- /** Row rendering snippet */
22
- row: Snippet<[T, number]>;
14
+ rowKey?: string;
23
15
  class?: string;
16
+ rowSnippet?: Snippet<[Record<string, unknown>, number]>;
24
17
  }
25
- declare const VirtualTable: import("svelte").Component<Props<unknown>, {}, "">;
18
+ declare const VirtualTable: import("svelte").Component<Props, {}, "">;
26
19
  type VirtualTable = ReturnType<typeof VirtualTable>;
27
20
  export default VirtualTable;
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, type Snippet } from 'svelte';
3
+ import { cn } from '../utils.js';
4
+
5
+ interface WatermarkFont {
6
+ color?: string;
7
+ fontSize?: number;
8
+ fontWeight?: 'normal' | 'light' | 'weight' | 'bold' | number;
9
+ fontFamily?: string;
10
+ fontStyle?: 'none' | 'normal' | 'italic' | 'oblique';
11
+ }
12
+
13
+ interface Props {
14
+ content?: string | string[];
15
+ image?: string;
16
+ width?: number;
17
+ height?: number;
18
+ rotate?: number;
19
+ zIndex?: number;
20
+ gap?: [number, number];
21
+ offset?: [number, number];
22
+ opacity?: number;
23
+ font?: WatermarkFont;
24
+ antiTamper?: boolean;
25
+ class?: string;
26
+ children?: Snippet;
27
+ }
28
+
29
+ let {
30
+ content = 'CONFIDENTIAL',
31
+ image,
32
+ width = 120,
33
+ height = 64,
34
+ rotate = -22,
35
+ zIndex = 9,
36
+ gap = [100, 100],
37
+ offset = [gap[0] / 2, gap[1] / 2],
38
+ opacity = 0.12,
39
+ font = {},
40
+ antiTamper = true,
41
+ class: className = '',
42
+ children,
43
+ }: Props = $props();
44
+
45
+ let containerEl: HTMLDivElement | null = $state(null);
46
+ let base64Url = $state<string>('');
47
+ let watermarkRevision = $state(0);
48
+ let observer: MutationObserver | null = null;
49
+ let isCleaningUp = false;
50
+
51
+ function renderWatermark() {
52
+ if (typeof window === 'undefined') return;
53
+
54
+ const canvas = document.createElement('canvas');
55
+ const ctx = canvas.getContext('2d');
56
+ if (!ctx) return;
57
+
58
+ const ratio = window.devicePixelRatio || 1;
59
+ const [gapX, gapY] = gap;
60
+ const canvasWidth = (gapX + width) * ratio;
61
+ const canvasHeight = (gapY + height) * ratio;
62
+
63
+ canvas.width = canvasWidth;
64
+ canvas.height = canvasHeight;
65
+ canvas.style.width = `${gapX + width}px`;
66
+ canvas.style.height = `${gapY + height}px`;
67
+
68
+ ctx.scale(ratio, ratio);
69
+ ctx.translate(offset[0], offset[1]);
70
+ ctx.rotate((rotate * Math.PI) / 180);
71
+
72
+ if (image) {
73
+ const img = new Image();
74
+ img.crossOrigin = 'anonymous';
75
+ img.referrerPolicy = 'no-referrer';
76
+ img.onload = () => {
77
+ ctx.globalAlpha = opacity;
78
+ ctx.drawImage(img, -width / 2, -height / 2, width, height);
79
+ base64Url = canvas.toDataURL();
80
+ };
81
+ img.src = image;
82
+ } else {
83
+ const {
84
+ fontSize = 14,
85
+ fontFamily = 'sans-serif',
86
+ fontWeight = 'normal',
87
+ fontStyle = 'normal',
88
+ color = 'currentColor',
89
+ } = font;
90
+
91
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
92
+ ctx.fillStyle = color === 'currentColor' ? 'rgba(128, 128, 128, 0.85)' : color;
93
+ ctx.globalAlpha = opacity;
94
+ ctx.textBaseline = 'middle';
95
+ ctx.textAlign = 'center';
96
+
97
+ const contents = Array.isArray(content) ? content : [content];
98
+ const lineHeight = fontSize + 4;
99
+ const totalHeight = contents.length * lineHeight;
100
+ const startY = -(totalHeight / 2) + lineHeight / 2;
101
+
102
+ contents.forEach((text, i) => {
103
+ ctx.fillText(text, 0, startY + i * lineHeight);
104
+ });
105
+
106
+ base64Url = canvas.toDataURL();
107
+ }
108
+ }
109
+
110
+ $effect(() => {
111
+ // Re-generate watermark when inputs change
112
+ if (content || image || width || height || rotate || gap || offset || opacity || font) {
113
+ renderWatermark();
114
+ }
115
+ });
116
+
117
+ onMount(() => {
118
+ renderWatermark();
119
+
120
+ if (antiTamper && typeof MutationObserver !== 'undefined' && containerEl) {
121
+ observer = new MutationObserver((mutations) => {
122
+ if (isCleaningUp) return;
123
+ for (const mutation of mutations) {
124
+ if (mutation.type === 'childList' || mutation.type === 'attributes') {
125
+ watermarkRevision += 1;
126
+ break;
127
+ }
128
+ }
129
+ });
130
+
131
+ observer.observe(containerEl, {
132
+ attributes: true,
133
+ subtree: true,
134
+ childList: true,
135
+ attributeFilter: ['style', 'class', 'hidden'],
136
+ });
137
+ }
138
+ });
139
+
140
+ onDestroy(() => {
141
+ isCleaningUp = true;
142
+ if (observer) {
143
+ observer.disconnect();
144
+ observer = null;
145
+ }
146
+ });
147
+ </script>
148
+
149
+ <div
150
+ bind:this={containerEl}
151
+ class={cn('relative overflow-hidden', className)}
152
+ data-svadmin-watermark-wrapper
153
+ >
154
+ {#key watermarkRevision}
155
+ {#if base64Url}
156
+ <div
157
+ data-svadmin-watermark
158
+ class="absolute inset-0 pointer-events-none"
159
+ style="z-index: {zIndex}; background-image: url('{base64Url}'); background-position: {offset[0]}px {offset[1]}px; background-repeat: repeat;"
160
+ ></div>
161
+ {/if}
162
+ {/key}
163
+ {#if children}
164
+ {@render children()}
165
+ {/if}
166
+ </div>
@@ -0,0 +1,26 @@
1
+ import { type Snippet } from 'svelte';
2
+ interface WatermarkFont {
3
+ color?: string;
4
+ fontSize?: number;
5
+ fontWeight?: 'normal' | 'light' | 'weight' | 'bold' | number;
6
+ fontFamily?: string;
7
+ fontStyle?: 'none' | 'normal' | 'italic' | 'oblique';
8
+ }
9
+ interface Props {
10
+ content?: string | string[];
11
+ image?: string;
12
+ width?: number;
13
+ height?: number;
14
+ rotate?: number;
15
+ zIndex?: number;
16
+ gap?: [number, number];
17
+ offset?: [number, number];
18
+ opacity?: number;
19
+ font?: WatermarkFont;
20
+ antiTamper?: boolean;
21
+ class?: string;
22
+ children?: Snippet;
23
+ }
24
+ declare const Watermark: import("svelte").Component<Props, {}, "">;
25
+ type Watermark = ReturnType<typeof Watermark>;
26
+ export default Watermark;
@@ -0,0 +1,11 @@
1
+ import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
2
+ import Root from "./navigation-menu.svelte";
3
+ import List from "./navigation-menu-list.svelte";
4
+ import Item from "./navigation-menu-item.svelte";
5
+ import Trigger from "./navigation-menu-trigger.svelte";
6
+ import Content from "./navigation-menu-content.svelte";
7
+ import Link from "./navigation-menu-link.svelte";
8
+ import Viewport from "./navigation-menu-viewport.svelte";
9
+ import Indicator from "./navigation-menu-indicator.svelte";
10
+ declare const Sub: import("svelte").Component<NavigationMenuPrimitive.SubProps, {}, "value" | "ref">;
11
+ export { Root, List, Item, Trigger, Content, Link, Viewport, Indicator, Sub, Root as NavigationMenu, List as NavigationMenuList, Item as NavigationMenuItem, Trigger as NavigationMenuTrigger, Content as NavigationMenuContent, Link as NavigationMenuLink, Viewport as NavigationMenuViewport, Indicator as NavigationMenuIndicator, Sub as NavigationMenuSub, };
@@ -0,0 +1,13 @@
1
+ import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
2
+ import Root from "./navigation-menu.svelte";
3
+ import List from "./navigation-menu-list.svelte";
4
+ import Item from "./navigation-menu-item.svelte";
5
+ import Trigger from "./navigation-menu-trigger.svelte";
6
+ import Content from "./navigation-menu-content.svelte";
7
+ import Link from "./navigation-menu-link.svelte";
8
+ import Viewport from "./navigation-menu-viewport.svelte";
9
+ import Indicator from "./navigation-menu-indicator.svelte";
10
+ const Sub = NavigationMenuPrimitive.Sub;
11
+ export { Root, List, Item, Trigger, Content, Link, Viewport, Indicator, Sub,
12
+ //
13
+ Root as NavigationMenu, List as NavigationMenuList, Item as NavigationMenuItem, Trigger as NavigationMenuTrigger, Content as NavigationMenuContent, Link as NavigationMenuLink, Viewport as NavigationMenuViewport, Indicator as NavigationMenuIndicator, Sub as NavigationMenuSub, };
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
3
+ import { cn } from "../../../utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: NavigationMenuPrimitive.ContentProps = $props();
10
+ </script>
11
+
12
+ <NavigationMenuPrimitive.Content
13
+ bind:ref
14
+ data-slot="navigation-menu-content"
15
+ class={cn(
16
+ "top-0 left-0 w-full md:absolute md:w-auto",
17
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ />
@@ -0,0 +1,4 @@
1
+ import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
2
+ declare const NavigationMenuContent: import("svelte").Component<NavigationMenuPrimitive.ContentProps, {}, "ref">;
3
+ type NavigationMenuContent = ReturnType<typeof NavigationMenuContent>;
4
+ export default NavigationMenuContent;