@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,195 @@
1
+ <script lang="ts">
2
+ import * as Dialog from './ui/dialog/index.js';
3
+ import { Button } from './ui/button/index.js';
4
+ import { Badge } from './ui/badge/index.js';
5
+ import { Upload, Image as ImageIcon, Check, Search, Folder } from '@lucide/svelte';
6
+ import { cn } from '../utils.js';
7
+
8
+ export interface MediaItem {
9
+ id: string;
10
+ name: string;
11
+ url: string;
12
+ size?: string;
13
+ type?: string;
14
+ category?: string;
15
+ }
16
+
17
+ interface Props {
18
+ open?: boolean;
19
+ title?: string;
20
+ multiple?: boolean;
21
+ selectedUrls?: string[];
22
+ mediaItems?: MediaItem[];
23
+ categories?: string[];
24
+ onselect?: (urls: string[]) => void;
25
+ onupload?: (files: File[]) => void | Promise<void>;
26
+ class?: string;
27
+ }
28
+
29
+ let {
30
+ open = $bindable(false),
31
+ title = 'Media Asset Library',
32
+ multiple = false,
33
+ selectedUrls = $bindable([]),
34
+ mediaItems = [],
35
+ categories = ['All', 'Images', 'Documents', 'Banners'],
36
+ onselect,
37
+ onupload,
38
+ class: className = '',
39
+ }: Props = $props();
40
+
41
+ let activeCategory = $state('All');
42
+ let searchQuery = $state('');
43
+ let localSelected = $state<string[]>([...selectedUrls]);
44
+
45
+ const filteredMedia = $derived(
46
+ mediaItems.filter((item) => {
47
+ const matchCat = activeCategory === 'All' || item.category === activeCategory;
48
+ const matchSearch = !searchQuery.trim() || item.name.toLowerCase().includes(searchQuery.toLowerCase().trim());
49
+ return matchCat && matchSearch;
50
+ })
51
+ );
52
+
53
+ function toggleSelect(url: string) {
54
+ if (multiple) {
55
+ if (localSelected.includes(url)) {
56
+ localSelected = localSelected.filter((u) => u !== url);
57
+ } else {
58
+ localSelected = [...localSelected, url];
59
+ }
60
+ } else {
61
+ localSelected = [url];
62
+ }
63
+ }
64
+
65
+ function handleConfirm() {
66
+ selectedUrls = [...localSelected];
67
+ onselect?.(selectedUrls);
68
+ open = false;
69
+ }
70
+
71
+ function handleFileInput(e: Event) {
72
+ const target = e.currentTarget as HTMLInputElement;
73
+ if (target.files?.length) {
74
+ onupload?.(Array.from(target.files));
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <Dialog.Root bind:open>
80
+ <Dialog.Content class={cn('sm:max-w-3xl max-h-[85vh] flex flex-col p-0 overflow-hidden text-xs', className)}>
81
+ <!-- Header -->
82
+ <Dialog.Header class="p-4 border-b border-border/60">
83
+ <Dialog.Title class="flex items-center gap-2">
84
+ <ImageIcon class="h-4 w-4 text-primary" />
85
+ <span>{title}</span>
86
+ </Dialog.Title>
87
+ </Dialog.Header>
88
+
89
+ <!-- Toolbar: Search & Categories -->
90
+ <div class="flex flex-wrap items-center justify-between gap-3 p-3 bg-muted/20 border-b border-border/60">
91
+ <div class="flex items-center gap-1.5 overflow-x-auto">
92
+ {#each categories as cat (cat)}
93
+ <button
94
+ type="button"
95
+ class={cn(
96
+ 'px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer border-0',
97
+ activeCategory === cat
98
+ ? 'bg-primary text-primary-foreground'
99
+ : 'bg-card text-muted-foreground hover:text-foreground hover:bg-muted/60'
100
+ )}
101
+ onclick={() => { activeCategory = cat; }}
102
+ >
103
+ {cat}
104
+ </button>
105
+ {/each}
106
+ </div>
107
+
108
+ <div class="flex items-center gap-2">
109
+ <div class="relative w-48">
110
+ <Search class="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
111
+ <input
112
+ type="text"
113
+ bind:value={searchQuery}
114
+ placeholder="Search media..."
115
+ class="h-7.5 w-full rounded-md border border-input bg-background pl-7.5 pr-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
116
+ />
117
+ </div>
118
+
119
+ <label class="inline-flex">
120
+ <input type="file" multiple class="hidden" onchange={handleFileInput} />
121
+ <span class="inline-flex items-center gap-1 h-7.5 px-2.5 rounded-md bg-secondary text-secondary-foreground text-xs font-medium hover:bg-secondary/80 cursor-pointer">
122
+ <Upload class="h-3.5 w-3.5" />
123
+ Upload
124
+ </span>
125
+ </label>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Media Grid -->
130
+ <div class="flex-1 overflow-y-auto p-4 min-h-64">
131
+ <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
132
+ {#each filteredMedia as item (item.id)}
133
+ {@const isChosen = localSelected.includes(item.url)}
134
+ <button
135
+ type="button"
136
+ class={cn(
137
+ 'group relative flex flex-col rounded-lg border text-left overflow-hidden transition-all cursor-pointer p-0 bg-card',
138
+ isChosen
139
+ ? 'border-primary ring-2 ring-primary/30 shadow-xs'
140
+ : 'border-border/60 hover:border-border hover:shadow-xs'
141
+ )}
142
+ onclick={() => toggleSelect(item.url)}
143
+ >
144
+ <!-- Thumbnail preview -->
145
+ <div class="relative h-28 w-full bg-muted/40 flex items-center justify-center overflow-hidden">
146
+ {#if item.type?.startsWith('image') || item.url.match(/\.(jpeg|jpg|gif|png|webp|svg)$/i)}
147
+ <img src={item.url} alt={item.name} class="h-full w-full object-cover group-hover:scale-105 transition-transform duration-200" />
148
+ {:else}
149
+ <Folder class="h-8 w-8 text-muted-foreground/60" />
150
+ {/if}
151
+
152
+ <!-- Select Checkmark Badge -->
153
+ {#if isChosen}
154
+ <div class="absolute top-1.5 right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-xs">
155
+ <Check class="h-3 w-3" />
156
+ </div>
157
+ {/if}
158
+ </div>
159
+
160
+ <!-- Metadata Info -->
161
+ <div class="p-2 space-y-0.5">
162
+ <div class="font-medium text-foreground truncate">{item.name}</div>
163
+ <div class="flex items-center justify-between text-[10px] text-muted-foreground">
164
+ <span>{item.size ?? '—'}</span>
165
+ {#if item.category}
166
+ <Badge variant="secondary" class="text-[9px] px-1 py-0">{item.category}</Badge>
167
+ {/if}
168
+ </div>
169
+ </div>
170
+ </button>
171
+ {/each}
172
+ </div>
173
+
174
+ {#if filteredMedia.length === 0}
175
+ <div class="flex flex-col items-center justify-center py-16 text-muted-foreground text-center space-y-2">
176
+ <ImageIcon class="h-8 w-8 opacity-40" />
177
+ <p>No media files found</p>
178
+ </div>
179
+ {/if}
180
+ </div>
181
+
182
+ <!-- Footer -->
183
+ <Dialog.Footer class="p-3 border-t border-border/60 bg-muted/20 flex items-center justify-between">
184
+ <div class="text-muted-foreground">
185
+ Selected <strong class="text-foreground">{localSelected.length}</strong> file(s)
186
+ </div>
187
+ <div class="flex items-center gap-2">
188
+ <Button variant="outline" size="sm" onclick={() => { open = false; }}>Cancel</Button>
189
+ <Button size="sm" disabled={localSelected.length === 0} onclick={handleConfirm}>
190
+ Insert Selected
191
+ </Button>
192
+ </div>
193
+ </Dialog.Footer>
194
+ </Dialog.Content>
195
+ </Dialog.Root>
@@ -0,0 +1,22 @@
1
+ export interface MediaItem {
2
+ id: string;
3
+ name: string;
4
+ url: string;
5
+ size?: string;
6
+ type?: string;
7
+ category?: string;
8
+ }
9
+ interface Props {
10
+ open?: boolean;
11
+ title?: string;
12
+ multiple?: boolean;
13
+ selectedUrls?: string[];
14
+ mediaItems?: MediaItem[];
15
+ categories?: string[];
16
+ onselect?: (urls: string[]) => void;
17
+ onupload?: (files: File[]) => void | Promise<void>;
18
+ class?: string;
19
+ }
20
+ declare const MediaLibraryModal: import("svelte").Component<Props, {}, "open" | "selectedUrls">;
21
+ type MediaLibraryModal = ReturnType<typeof MediaLibraryModal>;
22
+ export default MediaLibraryModal;
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+ import { Badge } from './ui/badge/index.js';
4
+ import { User, Hash } from '@lucide/svelte';
5
+
6
+ export interface MentionOption {
7
+ id: string;
8
+ label: string;
9
+ type?: 'user' | 'tag' | 'entity';
10
+ avatar?: string;
11
+ subtitle?: string;
12
+ }
13
+
14
+ interface Props {
15
+ value?: string;
16
+ placeholder?: string;
17
+ triggers?: string[];
18
+ users?: MentionOption[];
19
+ tags?: MentionOption[];
20
+ rows?: number;
21
+ disabled?: boolean;
22
+ onchange?: (value: string) => void;
23
+ class?: string;
24
+ }
25
+
26
+ let {
27
+ value = $bindable(''),
28
+ placeholder = 'Type @ to mention team members, or # to link tags...',
29
+ triggers = ['@', '#'],
30
+ users = [],
31
+ tags = [],
32
+ rows = 3,
33
+ disabled = false,
34
+ onchange,
35
+ class: className = '',
36
+ }: Props = $props();
37
+
38
+ let textareaEl: HTMLTextAreaElement | undefined = $state();
39
+ let showDropdown = $state(false);
40
+ let activeTrigger = $state<string | null>(null);
41
+ let searchQuery = $state('');
42
+ let selectedIndex = $state(0);
43
+ let triggerIndex = $state(-1);
44
+
45
+ const activeOptions = $derived<MentionOption[]>(
46
+ (() => {
47
+ const list = activeTrigger === '#' ? tags : users;
48
+ if (!searchQuery.trim()) return list;
49
+ return list.filter((item) =>
50
+ item.label.toLowerCase().includes(searchQuery.toLowerCase().trim())
51
+ );
52
+ })()
53
+ );
54
+
55
+ function handleInput(e: Event) {
56
+ const target = e.currentTarget as HTMLTextAreaElement;
57
+ value = target.value;
58
+ onchange?.(value);
59
+
60
+ const cursorPos = target.selectionStart ?? 0;
61
+ const textBeforeCursor = value.slice(0, cursorPos);
62
+
63
+ let matchTrigger: string | null = null;
64
+ let matchIdx = -1;
65
+
66
+ for (const trig of triggers) {
67
+ const idx = textBeforeCursor.lastIndexOf(trig);
68
+ if (idx !== -1 && (idx === 0 || /\s/.test(textBeforeCursor[idx - 1]))) {
69
+ const query = textBeforeCursor.slice(idx + 1);
70
+ if (!/\s/.test(query)) {
71
+ matchTrigger = trig;
72
+ matchIdx = idx;
73
+ searchQuery = query;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+
79
+ if (matchTrigger !== null) {
80
+ activeTrigger = matchTrigger;
81
+ triggerIndex = matchIdx;
82
+ showDropdown = true;
83
+ selectedIndex = 0;
84
+ } else {
85
+ showDropdown = false;
86
+ activeTrigger = null;
87
+ }
88
+ }
89
+
90
+ function insertMention(opt: MentionOption) {
91
+ if (!textareaEl || triggerIndex === -1 || !activeTrigger) return;
92
+ const before = value.slice(0, triggerIndex);
93
+ const after = value.slice(textareaEl.selectionStart ?? triggerIndex);
94
+ const mentionToken = `${activeTrigger}${opt.label} `;
95
+ value = `${before}${mentionToken}${after}`;
96
+ onchange?.(value);
97
+ showDropdown = false;
98
+ activeTrigger = null;
99
+
100
+ setTimeout(() => {
101
+ if (textareaEl) {
102
+ const newPos = before.length + mentionToken.length;
103
+ textareaEl.focus();
104
+ textareaEl.setSelectionRange(newPos, newPos);
105
+ }
106
+ }, 0);
107
+ }
108
+
109
+ function handleKeyDown(e: KeyboardEvent) {
110
+ if (!showDropdown || activeOptions.length === 0) return;
111
+
112
+ if (e.key === 'ArrowDown') {
113
+ e.preventDefault();
114
+ selectedIndex = (selectedIndex + 1) % activeOptions.length;
115
+ } else if (e.key === 'ArrowUp') {
116
+ e.preventDefault();
117
+ selectedIndex = (selectedIndex - 1 + activeOptions.length) % activeOptions.length;
118
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
119
+ e.preventDefault();
120
+ if (activeOptions[selectedIndex]) {
121
+ insertMention(activeOptions[selectedIndex]);
122
+ }
123
+ } else if (e.key === 'Escape') {
124
+ showDropdown = false;
125
+ activeTrigger = null;
126
+ }
127
+ }
128
+ </script>
129
+
130
+ <div class={cn('relative w-full text-xs', className)}>
131
+ <textarea
132
+ bind:this={textareaEl}
133
+ {value}
134
+ {placeholder}
135
+ {rows}
136
+ {disabled}
137
+ class="w-full rounded-md border border-input bg-background p-2.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-y"
138
+ oninput={handleInput}
139
+ onkeydown={handleKeyDown}
140
+ ></textarea>
141
+
142
+ {#if showDropdown && activeOptions.length > 0}
143
+ <div
144
+ class="absolute left-0 top-full mt-1 z-50 min-w-48 max-h-56 overflow-y-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-md p-1 animate-in fade-in-0 zoom-in-95"
145
+ >
146
+ <div class="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
147
+ {activeTrigger === '#' ? 'Select Tag' : 'Mention Member'}
148
+ </div>
149
+ {#each activeOptions as opt, idx (opt.id)}
150
+ <button
151
+ type="button"
152
+ class={cn(
153
+ 'w-full flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-left text-xs transition-colors cursor-pointer border-0',
154
+ idx === selectedIndex ? 'bg-accent text-accent-foreground font-medium' : 'bg-transparent text-foreground hover:bg-muted/60'
155
+ )}
156
+ onclick={() => insertMention(opt)}
157
+ >
158
+ <div class="flex items-center gap-2 truncate">
159
+ {#if activeTrigger === '#'}
160
+ <Hash class="h-3.5 w-3.5 text-primary shrink-0" />
161
+ {:else}
162
+ <User class="h-3.5 w-3.5 text-primary shrink-0" />
163
+ {/if}
164
+ <span class="truncate">{opt.label}</span>
165
+ </div>
166
+
167
+ {#if opt.subtitle}
168
+ <Badge variant="secondary" class="text-[10px] px-1 py-0">{opt.subtitle}</Badge>
169
+ {/if}
170
+ </button>
171
+ {/each}
172
+ </div>
173
+ {/if}
174
+ </div>
@@ -0,0 +1,21 @@
1
+ export interface MentionOption {
2
+ id: string;
3
+ label: string;
4
+ type?: 'user' | 'tag' | 'entity';
5
+ avatar?: string;
6
+ subtitle?: string;
7
+ }
8
+ interface Props {
9
+ value?: string;
10
+ placeholder?: string;
11
+ triggers?: string[];
12
+ users?: MentionOption[];
13
+ tags?: MentionOption[];
14
+ rows?: number;
15
+ disabled?: boolean;
16
+ onchange?: (value: string) => void;
17
+ class?: string;
18
+ }
19
+ declare const MentionsInput: import("svelte").Component<Props, {}, "value">;
20
+ type MentionsInput = ReturnType<typeof MentionsInput>;
21
+ export default MentionsInput;
@@ -1,38 +1,123 @@
1
1
  <script lang="ts">
2
- import { getResource } from '@svadmin/core';
3
- import { useTranslation } from '@svadmin/core/i18n';
2
+ import type { Snippet } from 'svelte';
4
3
  import * as Dialog from './ui/dialog/index.js';
5
- import AutoForm from './AutoForm.svelte';
4
+ import { Button } from './ui/button/index.js';
5
+ import { Loader2 } from '@lucide/svelte';
6
+ import { cn } from '../utils.js';
6
7
 
7
- const i18n = useTranslation();
8
+ interface Props {
9
+ open?: boolean;
10
+ title: string;
11
+ description?: string;
12
+ triggerText?: string;
13
+ triggerVariant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive';
14
+ triggerSize?: 'default' | 'sm' | 'lg' | 'icon';
15
+ submitText?: string;
16
+ cancelText?: string;
17
+ widthClass?: string;
18
+ isSubmitting?: boolean;
19
+ disabled?: boolean;
20
+ onsubmit?: () => void | Promise<void>;
21
+ oncancel?: () => void;
22
+ class?: string;
23
+ triggerSnippet?: Snippet;
24
+ children?: Snippet;
25
+ }
26
+
27
+ let {
28
+ open = $bindable(false),
29
+ title,
30
+ description,
31
+ triggerText = 'Open Form',
32
+ triggerVariant = 'default',
33
+ triggerSize = 'sm',
34
+ submitText = 'Confirm',
35
+ cancelText = 'Cancel',
36
+ widthClass = 'sm:max-w-lg',
37
+ isSubmitting = false,
38
+ disabled = false,
39
+ onsubmit,
40
+ oncancel,
41
+ class: className = '',
42
+ triggerSnippet,
43
+ children,
44
+ }: Props = $props();
8
45
 
9
- let { resourceName, mode = 'create', id, open = $bindable(false), onSuccess } = $props<{
10
- resourceName: string;
11
- mode?: 'create' | 'edit';
12
- id?: string | number;
13
- open: boolean;
14
- onSuccess?: () => void;
15
- }>();
46
+ let loading = $state(false);
16
47
 
17
- const resource = $derived(getResource(resourceName));
18
- const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
19
- const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
48
+ async function handleSubmit(e: SubmitEvent) {
49
+ e.preventDefault();
50
+ loading = true;
51
+ try {
52
+ await onsubmit?.();
53
+ open = false;
54
+ } finally {
55
+ loading = false;
56
+ }
57
+ }
20
58
 
21
- function handleSuccess() {
59
+ function handleCancel() {
60
+ oncancel?.();
22
61
  open = false;
23
- onSuccess?.();
24
62
  }
25
63
  </script>
26
64
 
27
- {#if open}
28
- <Dialog.Dialog bind:open>
29
- <Dialog.DialogContent class="{dialogWidth} max-h-[85vh] overflow-y-auto">
30
- <Dialog.DialogHeader>
31
- <Dialog.DialogTitle>
32
- {mode === 'create' ? `${i18n.t('common.create')}${resource.label}` : `${i18n.t('common.edit')}${resource.label}`}
33
- </Dialog.DialogTitle>
34
- </Dialog.DialogHeader>
35
- <AutoForm {resourceName} {mode} {id} showHeader={false} onSuccess={handleSuccess} />
36
- </Dialog.DialogContent>
37
- </Dialog.Dialog>
38
- {/if}
65
+ <Dialog.Root bind:open>
66
+ {#if triggerSnippet}
67
+ <Dialog.Trigger>
68
+ {#snippet child({ props })}
69
+ <div {...props} class="inline-flex">
70
+ {@render triggerSnippet()}
71
+ </div>
72
+ {/snippet}
73
+ </Dialog.Trigger>
74
+ {:else if triggerText}
75
+ <Dialog.Trigger>
76
+ {#snippet child({ props })}
77
+ <Button {...props} variant={triggerVariant} size={triggerSize} {disabled}>
78
+ {triggerText}
79
+ </Button>
80
+ {/snippet}
81
+ </Dialog.Trigger>
82
+ {/if}
83
+
84
+ <Dialog.Content class={cn(widthClass, 'max-h-[90vh] overflow-y-auto', className)}>
85
+ <form onsubmit={handleSubmit} class="space-y-4">
86
+ <Dialog.Header>
87
+ <Dialog.Title>{title}</Dialog.Title>
88
+ {#if description}
89
+ <Dialog.Description>{description}</Dialog.Description>
90
+ {/if}
91
+ </Dialog.Header>
92
+
93
+ <div class="py-2 text-sm text-foreground">
94
+ {#if children}
95
+ {@render children()}
96
+ {/if}
97
+ </div>
98
+
99
+ <Dialog.Footer class="gap-2 pt-2 border-t border-border/50">
100
+ <Button
101
+ type="button"
102
+ variant="outline"
103
+ size="sm"
104
+ disabled={loading || isSubmitting}
105
+ onclick={handleCancel}
106
+ >
107
+ {cancelText}
108
+ </Button>
109
+ <Button
110
+ type="submit"
111
+ size="sm"
112
+ disabled={loading || isSubmitting || disabled}
113
+ class="gap-1.5 min-w-20"
114
+ >
115
+ {#if loading || isSubmitting}
116
+ <Loader2 class="h-4 w-4 animate-spin" />
117
+ {/if}
118
+ {submitText}
119
+ </Button>
120
+ </Dialog.Footer>
121
+ </form>
122
+ </Dialog.Content>
123
+ </Dialog.Root>
@@ -1,10 +1,22 @@
1
- type $$ComponentProps = {
2
- resourceName: string;
3
- mode?: 'create' | 'edit';
4
- id?: string | number;
5
- open: boolean;
6
- onSuccess?: () => void;
7
- };
8
- declare const ModalForm: import("svelte").Component<$$ComponentProps, {}, "open">;
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ open?: boolean;
4
+ title: string;
5
+ description?: string;
6
+ triggerText?: string;
7
+ triggerVariant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive';
8
+ triggerSize?: 'default' | 'sm' | 'lg' | 'icon';
9
+ submitText?: string;
10
+ cancelText?: string;
11
+ widthClass?: string;
12
+ isSubmitting?: boolean;
13
+ disabled?: boolean;
14
+ onsubmit?: () => void | Promise<void>;
15
+ oncancel?: () => void;
16
+ class?: string;
17
+ triggerSnippet?: Snippet;
18
+ children?: Snippet;
19
+ }
20
+ declare const ModalForm: import("svelte").Component<Props, {}, "open">;
9
21
  type ModalForm = ReturnType<typeof ModalForm>;
10
22
  export default ModalForm;