@svadmin/ui 0.3.0 → 0.5.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 (110) hide show
  1. package/package.json +9 -4
  2. package/src/actions.ts +110 -0
  3. package/src/components/AdminApp.svelte +7 -4
  4. package/src/components/Authenticated.svelte +1 -7
  5. package/src/components/AutoForm.svelte +158 -69
  6. package/src/components/AutoSaveIndicator.svelte +19 -14
  7. package/src/components/AutoTable.svelte +465 -243
  8. package/src/components/Breadcrumbs.svelte +20 -17
  9. package/src/components/CanAccess.svelte +6 -4
  10. package/src/components/ComboboxField.svelte +122 -0
  11. package/src/components/CommandPalette.svelte +93 -0
  12. package/src/components/ConfigErrorScreen.svelte +53 -151
  13. package/src/components/ConfirmDialog.svelte +25 -17
  14. package/src/components/CreatePage.svelte +3 -2
  15. package/src/components/DevTools.svelte +99 -230
  16. package/src/components/DrawerForm.svelte +19 -10
  17. package/src/components/EditPage.svelte +25 -6
  18. package/src/components/EmptyState.svelte +5 -1
  19. package/src/components/ErrorBoundary.svelte +38 -40
  20. package/src/components/ErrorComponent.svelte +18 -17
  21. package/src/components/FieldRenderer.svelte +240 -75
  22. package/src/components/ForgotPasswordPage.svelte +20 -111
  23. package/src/components/Header.svelte +28 -8
  24. package/src/components/InferencerPanel.svelte +136 -110
  25. package/src/components/InfiniteList.svelte +93 -0
  26. package/src/components/Layout.svelte +85 -22
  27. package/src/components/ListPage.svelte +25 -1
  28. package/src/components/LiveIndicator.svelte +30 -87
  29. package/src/components/LoginPage.svelte +81 -155
  30. package/src/components/ModalForm.svelte +6 -2
  31. package/src/components/PageSkeleton.svelte +2 -1
  32. package/src/components/PasswordInput.svelte +95 -0
  33. package/src/components/ProfilePage.svelte +166 -0
  34. package/src/components/RegisterPage.svelte +35 -164
  35. package/src/components/ShowPage.svelte +32 -33
  36. package/src/components/Sidebar.svelte +185 -73
  37. package/src/components/StatsCard.svelte +12 -3
  38. package/src/components/StepsForm.svelte +139 -0
  39. package/src/components/Toast.svelte +15 -54
  40. package/src/components/TooltipButton.svelte +52 -0
  41. package/src/components/UndoableNotification.svelte +28 -96
  42. package/src/components/UpdatePasswordPage.svelte +32 -160
  43. package/src/components/buttons/RefreshButton.svelte +7 -2
  44. package/src/components/fieldComponentMap.ts +79 -0
  45. package/src/components/fields/BooleanField.svelte +9 -9
  46. package/src/components/fields/EmailField.svelte +2 -8
  47. package/src/components/fields/FileField.svelte +4 -9
  48. package/src/components/fields/ImageField.svelte +2 -7
  49. package/src/components/fields/JsonField.svelte +72 -0
  50. package/src/components/fields/MarkdownField.svelte +2 -12
  51. package/src/components/fields/MultiSelectField.svelte +20 -0
  52. package/src/components/fields/RelationField.svelte +24 -0
  53. package/src/components/fields/RichTextField.svelte +35 -0
  54. package/src/components/fields/SelectField.svelte +14 -0
  55. package/src/components/fields/TagField.svelte +6 -11
  56. package/src/components/fields/TextField.svelte +2 -7
  57. package/src/components/fields/UrlField.svelte +4 -10
  58. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
  59. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
  60. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
  61. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
  62. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
  63. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
  64. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
  65. package/src/components/ui/alert-dialog/index.ts +20 -0
  66. package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
  67. package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
  68. package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
  69. package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
  70. package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
  71. package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
  72. package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
  73. package/src/components/ui/breadcrumb/index.ts +7 -0
  74. package/src/components/ui/collapsible/index.ts +7 -0
  75. package/src/components/ui/command/command-dialog.svelte +31 -0
  76. package/src/components/ui/command/index.ts +2 -0
  77. package/src/components/ui/context-menu/index.ts +11 -0
  78. package/src/components/ui/drawer/drawer-content.svelte +21 -0
  79. package/src/components/ui/drawer/drawer-description.svelte +11 -0
  80. package/src/components/ui/drawer/drawer-footer.svelte +11 -0
  81. package/src/components/ui/drawer/drawer-header.svelte +11 -0
  82. package/src/components/ui/drawer/drawer-title.svelte +11 -0
  83. package/src/components/ui/drawer/drawer.svelte +18 -0
  84. package/src/components/ui/drawer/index.ts +6 -0
  85. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
  86. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
  87. package/src/components/ui/dropdown-menu/index.ts +6 -0
  88. package/src/components/ui/label/index.ts +1 -0
  89. package/src/components/ui/label/label.svelte +18 -0
  90. package/src/components/ui/pagination/index.ts +7 -0
  91. package/src/components/ui/pagination/pagination-content.svelte +11 -0
  92. package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
  93. package/src/components/ui/pagination/pagination-item.svelte +11 -0
  94. package/src/components/ui/pagination/pagination-link.svelte +26 -0
  95. package/src/components/ui/pagination/pagination-next.svelte +22 -0
  96. package/src/components/ui/pagination/pagination-previous.svelte +22 -0
  97. package/src/components/ui/pagination/pagination.svelte +11 -0
  98. package/src/components/ui/popover/index.ts +12 -0
  99. package/src/components/ui/popover/popover-content.svelte +25 -0
  100. package/src/components/ui/progress/index.ts +1 -0
  101. package/src/components/ui/progress/progress.svelte +22 -0
  102. package/src/components/ui/scroll-area/index.ts +1 -0
  103. package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
  104. package/src/components/ui/sheet/index.ts +15 -1
  105. package/src/components/ui/sheet/sheet-content.svelte +28 -0
  106. package/src/components/ui/sheet/sheet-description.svelte +22 -0
  107. package/src/components/ui/sheet/sheet-footer.svelte +22 -0
  108. package/src/components/ui/sheet/sheet-header.svelte +22 -0
  109. package/src/components/ui/sheet/sheet-title.svelte +22 -0
  110. package/src/index.ts +31 -1
@@ -1,280 +1,149 @@
1
1
  <script lang="ts">
2
+ import { fly, scale } from 'svelte/transition';
2
3
  import { getResources } from '@svadmin/core';
3
4
  import { getTheme, getColorTheme } from '@svadmin/core';
4
- import { getLocale } from '@svadmin/core/i18n';
5
+ import { getLocale, t } from '@svadmin/core/i18n';
5
6
  import { currentPath } from '@svadmin/core/router';
6
7
  import { Button } from './ui/button/index.js';
8
+ import TooltipButton from './TooltipButton.svelte';
9
+ import * as Tabs from './ui/tabs/index.js';
10
+ import { Badge } from './ui/badge/index.js';
11
+ import { ScrollArea } from './ui/scroll-area/index.js';
12
+ import { Separator } from './ui/separator/index.js';
7
13
  import { X, Bug, ChevronDown, ChevronUp, Wand2 } from 'lucide-svelte';
8
14
  import InferencerPanel from './InferencerPanel.svelte';
9
15
 
10
16
  let visible = $state(false);
11
17
  let collapsed = $state(false);
12
- let activeTab = $state<'state' | 'inferencer'>('state');
13
18
 
14
19
  function toggle() {
15
20
  visible = !visible;
16
21
  }
17
22
 
18
- // Keyboard shortcut: Ctrl+Shift+D
19
- if (typeof window !== 'undefined') {
20
- window.addEventListener('keydown', (e) => {
21
- if (e.ctrlKey && e.shiftKey && e.key === 'D') {
22
- e.preventDefault();
23
- toggle();
24
- }
25
- });
23
+ function handleKeydown(e: KeyboardEvent) {
24
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
25
+ e.preventDefault();
26
+ toggle();
27
+ }
26
28
  }
27
29
 
28
- const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
30
+ const resources = $derived.by(() => { try { return getResources(); } catch { return []; } });
29
31
  const path = $derived(currentPath());
30
32
  const theme = $derived(getTheme());
31
33
  const colorTheme = $derived(getColorTheme());
32
34
  const locale = $derived(getLocale());
35
+
36
+ // Auto-trace state changes in dev mode via Svelte 5 $inspect
37
+ if (import.meta.env?.DEV) {
38
+ $inspect({ path, theme, colorTheme, locale, resourceCount: resources.length });
39
+ }
33
40
  </script>
34
41
 
42
+ <svelte:window onkeydown={handleKeydown} />
43
+
35
44
  {#if import.meta.env.DEV}
36
45
  {#if visible}
37
- <div class="devtools-panel" class:collapsed>
38
- <div class="devtools-header">
39
- <div class="devtools-title">
46
+ <div
47
+ class="fixed bottom-0 right-4 z-[9999] w-[420px] max-w-[95vw] rounded-t-xl border border-b-0 bg-card shadow-2xl text-[0.8125rem] overflow-hidden"
48
+ class:w-auto={collapsed} class:min-w-[200px]={collapsed}
49
+ transition:fly={{ y: 400, duration: 300 }}
50
+ >
51
+ <!-- Header -->
52
+ <div class="flex items-center justify-between px-3 py-2 bg-muted border-b">
53
+ <div class="flex items-center gap-1.5 font-semibold text-xs uppercase tracking-wider text-foreground">
40
54
  <Bug class="h-4 w-4" />
41
55
  <span>svadmin DevTools</span>
42
56
  </div>
43
- <div class="devtools-header-actions">
44
- <button onclick={() => collapsed = !collapsed} class="devtools-btn">
57
+ <div class="flex gap-1">
58
+ <TooltipButton tooltip={collapsed ? t('common.expand') : t('common.collapse')} variant="ghost" size="icon" class="h-6 w-6" onclick={() => collapsed = !collapsed}>
45
59
  {#if collapsed}
46
60
  <ChevronUp class="h-3.5 w-3.5" />
47
61
  {:else}
48
62
  <ChevronDown class="h-3.5 w-3.5" />
49
63
  {/if}
50
- </button>
51
- <button onclick={toggle} class="devtools-btn">
64
+ </TooltipButton>
65
+ <TooltipButton tooltip={t('common.close')} variant="ghost" size="icon" class="h-6 w-6" onclick={toggle}>
52
66
  <X class="h-3.5 w-3.5" />
53
- </button>
67
+ </TooltipButton>
54
68
  </div>
55
69
  </div>
56
70
 
57
71
  {#if !collapsed}
58
- <div class="devtools-tabs">
59
- <button class="devtools-tab" class:active={activeTab === 'state'} onclick={() => activeTab = 'state'}>
60
- State
61
- </button>
62
- <button class="devtools-tab" class:active={activeTab === 'inferencer'} onclick={() => activeTab = 'inferencer'}>
63
- <Wand2 class="h-3 w-3" /> Inferencer
64
- </button>
65
- </div>
72
+ <Tabs.Root value="state" class="w-full">
73
+ <Tabs.List class="grid w-full grid-cols-2 h-8 rounded-none border-b">
74
+ <Tabs.Trigger value="state" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">State</Tabs.Trigger>
75
+ <Tabs.Trigger value="inferencer" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">
76
+ <Wand2 class="h-3 w-3 mr-1" /> Inferencer
77
+ </Tabs.Trigger>
78
+ </Tabs.List>
79
+ <Tabs.Content value="state" class="p-0">
80
+ <ScrollArea class="max-h-[400px]">
81
+ <div class="p-2 space-y-1">
82
+ <!-- Router -->
83
+ <div class="py-1">
84
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Router</h4>
85
+ <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
86
+ <span class="text-xs text-foreground">Path</span>
87
+ <Badge variant="secondary" class="font-mono text-[0.6875rem]">{path}</Badge>
88
+ </div>
89
+ </div>
66
90
 
67
- <div class="devtools-body">
68
- {#if activeTab === 'state'}
69
- <div class="devtools-section">
70
- <h4>Router</h4>
71
- <div class="devtools-row">
72
- <span class="devtools-label">Path</span>
73
- <code class="devtools-value">{path}</code>
74
- </div>
75
- </div>
91
+ <Separator />
92
+
93
+ <!-- Theme -->
94
+ <div class="py-1">
95
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Theme</h4>
96
+ <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
97
+ <span class="text-xs text-foreground">Mode</span>
98
+ <Badge variant="secondary" class="font-mono text-[0.6875rem]">{theme}</Badge>
99
+ </div>
100
+ <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
101
+ <span class="text-xs text-foreground">Color</span>
102
+ <Badge variant="secondary" class="font-mono text-[0.6875rem]">{colorTheme}</Badge>
103
+ </div>
104
+ </div>
76
105
 
77
- <div class="devtools-section">
78
- <h4>Theme</h4>
79
- <div class="devtools-row">
80
- <span class="devtools-label">Mode</span>
81
- <code class="devtools-value">{theme}</code>
82
- </div>
83
- <div class="devtools-row">
84
- <span class="devtools-label">Color</span>
85
- <code class="devtools-value">{colorTheme}</code>
86
- </div>
87
- </div>
106
+ <Separator />
88
107
 
89
- <div class="devtools-section">
90
- <h4>i18n</h4>
91
- <div class="devtools-row">
92
- <span class="devtools-label">Locale</span>
93
- <code class="devtools-value">{locale}</code>
94
- </div>
95
- </div>
108
+ <!-- i18n -->
109
+ <div class="py-1">
110
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">i18n</h4>
111
+ <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
112
+ <span class="text-xs text-foreground">Locale</span>
113
+ <Badge variant="secondary" class="font-mono text-[0.6875rem]">{locale}</Badge>
114
+ </div>
115
+ </div>
96
116
 
97
- <div class="devtools-section">
98
- <h4>Resources ({resources.length})</h4>
99
- {#each resources as r}
100
- <div class="devtools-row">
101
- <span class="devtools-label">{r.name}</span>
102
- <code class="devtools-value">{r.fields.length} fields</code>
117
+ <Separator />
118
+
119
+ <!-- Resources -->
120
+ <div class="py-1">
121
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Resources ({resources.length})</h4>
122
+ {#each resources as r}
123
+ <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
124
+ <span class="text-xs text-foreground">{r.name}</span>
125
+ <Badge variant="outline" class="font-mono text-[0.6875rem]">{r.fields.length} fields</Badge>
126
+ </div>
127
+ {/each}
103
128
  </div>
104
- {/each}
105
- </div>
106
- {:else}
129
+ </div>
130
+ </ScrollArea>
131
+ </Tabs.Content>
132
+ <Tabs.Content value="inferencer" class="p-0">
107
133
  <InferencerPanel />
108
- {/if}
109
- </div>
134
+ </Tabs.Content>
135
+ </Tabs.Root>
110
136
  {/if}
111
137
  </div>
112
138
  {:else}
113
- <button class="devtools-fab" onclick={toggle} title="DevTools (Ctrl+Shift+D)">
139
+ <Button
140
+ variant="default"
141
+ size="icon"
142
+ class="fixed bottom-4 right-4 z-[9999] h-9 w-9 rounded-full shadow-lg opacity-60 hover:opacity-100 hover:scale-110 transition-all"
143
+ onclick={toggle}
144
+ title={t('devtools.title')}
145
+ >
114
146
  <Bug class="h-4 w-4" />
115
- </button>
147
+ </Button>
116
148
  {/if}
117
149
  {/if}
118
-
119
- <style>
120
- .devtools-panel {
121
- position: fixed;
122
- bottom: 0;
123
- right: 1rem;
124
- z-index: 9999;
125
- width: 420px;
126
- max-width: 95vw;
127
- background: hsl(var(--card));
128
- border: 1px solid hsl(var(--border));
129
- border-bottom: none;
130
- border-radius: 0.75rem 0.75rem 0 0;
131
- box-shadow: 0 -4px 24px hsl(0 0% 0% / 0.12);
132
- font-size: 0.8125rem;
133
- overflow: hidden;
134
- }
135
-
136
- .devtools-panel.collapsed {
137
- width: auto;
138
- min-width: 200px;
139
- }
140
-
141
- .devtools-header {
142
- display: flex;
143
- align-items: center;
144
- justify-content: space-between;
145
- padding: 0.5rem 0.75rem;
146
- background: hsl(var(--muted));
147
- border-bottom: 1px solid hsl(var(--border));
148
- }
149
-
150
- .devtools-title {
151
- display: flex;
152
- align-items: center;
153
- gap: 0.375rem;
154
- font-weight: 600;
155
- font-size: 0.75rem;
156
- text-transform: uppercase;
157
- letter-spacing: 0.05em;
158
- color: hsl(var(--foreground));
159
- }
160
-
161
- .devtools-header-actions {
162
- display: flex;
163
- gap: 0.25rem;
164
- }
165
-
166
- .devtools-btn {
167
- background: none;
168
- border: none;
169
- cursor: pointer;
170
- padding: 0.25rem;
171
- color: hsl(var(--muted-foreground));
172
- border-radius: 0.25rem;
173
- }
174
- .devtools-btn:hover {
175
- background: hsl(var(--accent));
176
- color: hsl(var(--foreground));
177
- }
178
-
179
- .devtools-body {
180
- max-height: 400px;
181
- overflow-y: auto;
182
- padding: 0.5rem;
183
- }
184
-
185
- .devtools-section {
186
- padding: 0.375rem 0;
187
- }
188
-
189
- .devtools-section h4 {
190
- font-size: 0.6875rem;
191
- font-weight: 700;
192
- text-transform: uppercase;
193
- letter-spacing: 0.08em;
194
- color: hsl(var(--muted-foreground));
195
- margin: 0 0 0.25rem;
196
- padding: 0 0.25rem;
197
- }
198
-
199
- .devtools-row {
200
- display: flex;
201
- align-items: center;
202
- justify-content: space-between;
203
- padding: 0.1875rem 0.25rem;
204
- border-radius: 0.25rem;
205
- }
206
- .devtools-row:hover {
207
- background: hsl(var(--muted) / 0.5);
208
- }
209
-
210
- .devtools-label {
211
- color: hsl(var(--foreground));
212
- font-size: 0.75rem;
213
- }
214
-
215
- .devtools-value {
216
- font-size: 0.6875rem;
217
- color: hsl(var(--primary));
218
- background: hsl(var(--primary) / 0.1);
219
- padding: 0.125rem 0.375rem;
220
- border-radius: 0.25rem;
221
- font-family: monospace;
222
- }
223
-
224
- .devtools-fab {
225
- position: fixed;
226
- bottom: 1rem;
227
- right: 1rem;
228
- z-index: 9999;
229
- width: 36px;
230
- height: 36px;
231
- border-radius: 50%;
232
- background: hsl(var(--primary));
233
- color: hsl(var(--primary-foreground));
234
- border: none;
235
- cursor: pointer;
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
- box-shadow: 0 2px 8px hsl(var(--primary) / 0.3);
240
- transition: all 0.2s;
241
- opacity: 0.6;
242
- }
243
- .devtools-fab:hover {
244
- opacity: 1;
245
- transform: scale(1.1);
246
- }
247
- .devtools-tabs {
248
- display: flex;
249
- border-bottom: 1px solid hsl(var(--border));
250
- }
251
-
252
- .devtools-tab {
253
- flex: 1;
254
- display: flex;
255
- align-items: center;
256
- justify-content: center;
257
- gap: 0.25rem;
258
- padding: 0.375rem 0.5rem;
259
- font-size: 0.6875rem;
260
- font-weight: 600;
261
- text-transform: uppercase;
262
- letter-spacing: 0.05em;
263
- background: none;
264
- border: none;
265
- cursor: pointer;
266
- color: hsl(var(--muted-foreground));
267
- border-bottom: 2px solid transparent;
268
- transition: all 0.15s;
269
- }
270
-
271
- .devtools-tab:hover {
272
- color: hsl(var(--foreground));
273
- background: hsl(var(--muted) / 0.3);
274
- }
275
-
276
- .devtools-tab.active {
277
- color: hsl(var(--primary));
278
- border-bottom-color: hsl(var(--primary));
279
- }
280
- </style>
@@ -1,26 +1,35 @@
1
1
  <script lang="ts">
2
2
  import { getResource } from '@svadmin/core';
3
- import { Sheet } from './ui/sheet/index.js';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import * as Sheet from './ui/sheet/index.js';
4
5
  import AutoForm from './AutoForm.svelte';
5
6
 
6
- let { resourceName, mode = 'create', id, open = $bindable(false), side = 'right' } = $props<{
7
+ let { resourceName, mode = 'create', id, open = $bindable(false), side = 'right', onSuccess } = $props<{
7
8
  resourceName: string;
8
9
  mode?: 'create' | 'edit';
9
10
  id?: string | number;
10
11
  open: boolean;
11
12
  side?: 'left' | 'right';
13
+ onSuccess?: () => void;
12
14
  }>();
13
15
 
14
16
  const resource = $derived(getResource(resourceName));
17
+
18
+ function handleClose() {
19
+ open = false;
20
+ onSuccess?.();
21
+ }
15
22
  </script>
16
23
 
17
- {#if open}
18
- <Sheet bind:open {side}>
19
- <div class="p-6 space-y-4">
20
- <h2 class="text-lg font-semibold text-foreground">
21
- {mode === 'create' ? `Create ${resource.label}` : `Edit ${resource.label}`}
22
- </h2>
24
+ <Sheet.Root bind:open {side}>
25
+ <Sheet.Content>
26
+ <Sheet.Header>
27
+ <Sheet.Title>
28
+ {mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
29
+ </Sheet.Title>
30
+ </Sheet.Header>
31
+ <div class="flex-1 overflow-y-auto">
23
32
  <AutoForm {resourceName} {mode} {id} />
24
33
  </div>
25
- </Sheet>
26
- {/if}
34
+ </Sheet.Content>
35
+ </Sheet.Root>
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
- import { getResource } from '@svadmin/core';
2
+ import { getResource, useDelete } from '@svadmin/core';
3
3
  import { navigate } from '@svadmin/core/router';
4
4
  import { t } from '@svadmin/core/i18n';
5
5
  import type { Snippet } from 'svelte';
6
6
  import PageHeader from './PageHeader.svelte';
7
7
  import AutoForm from './AutoForm.svelte';
8
+ import ConfirmDialog from './ConfirmDialog.svelte';
8
9
  import { ArrowLeft, Trash2 } from 'lucide-svelte';
9
10
  import { Button } from './ui/button/index.js';
11
+ import TooltipButton from './TooltipButton.svelte';
10
12
 
11
13
  interface Props {
12
14
  resourceName: string;
@@ -29,18 +31,29 @@
29
31
  const resource = getResource(resourceName);
30
32
  const pageTitle = title ?? `${t('common.edit')} ${resource.label} #${id}`;
31
33
  const showDelete = canDelete ?? resource.canDelete !== false;
34
+
35
+ const deleteResult = useDelete({ resource: resourceName });
36
+ const deleteMutation = deleteResult.mutation;
37
+
38
+ let confirmOpen = $state(false);
39
+
40
+ async function handleDelete() {
41
+ await deleteMutation.mutateAsync({ id, resource: resourceName });
42
+ confirmOpen = false;
43
+ navigate(`/${resourceName}`);
44
+ }
32
45
  </script>
33
46
 
34
47
  <div class="space-y-6 {className}">
35
48
  <PageHeader title={pageTitle}>
36
49
  {#snippet actions()}
37
- <Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
50
+ <TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
38
51
  <ArrowLeft class="h-5 w-5" />
39
- </Button>
52
+ </TooltipButton>
40
53
  {#if showDelete}
41
- <Button variant="destructive" size="sm">
42
- <Trash2 class="h-4 w-4" data-icon="inline-start" /> {t('common.delete')}
43
- </Button>
54
+ <TooltipButton tooltip={t('common.delete')} variant="destructive" size="sm" onclick={() => { confirmOpen = true; }}>
55
+ <Trash2 class="h-4 w-4" />
56
+ </TooltipButton>
44
57
  {/if}
45
58
  {#if headerActions}
46
59
  {@render headerActions()}
@@ -50,3 +63,9 @@
50
63
 
51
64
  <AutoForm {resourceName} mode="edit" {id} />
52
65
  </div>
66
+
67
+ <ConfirmDialog
68
+ bind:open={confirmOpen}
69
+ title={t('common.deleteConfirm')}
70
+ onConfirm={handleDelete}
71
+ />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { fade, scale } from 'svelte/transition';
2
3
  import { PackageOpen } from 'lucide-svelte';
3
4
  import { t } from '@svadmin/core';
4
5
 
@@ -13,7 +14,10 @@
13
14
  }>();
14
15
  </script>
15
16
 
16
- <div class="flex flex-col items-center justify-center p-8 text-center min-h-[300px] bg-card rounded-lg border border-dashed border-border/50 shadow-sm transition-all duration-300">
17
+ <div
18
+ class="flex flex-col items-center justify-center p-8 text-center min-h-[300px] bg-card rounded-lg border border-dashed border-border/50 shadow-sm"
19
+ in:scale={{ start: 0.95, duration: 250, opacity: 0 }}
20
+ >
17
21
  <div class="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-4">
18
22
  <PackageOpen class="h-10 w-10 text-muted-foreground" />
19
23
  </div>
@@ -2,50 +2,48 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import { AlertTriangle } from 'lucide-svelte';
4
4
  import { Button } from './ui/button/index.js';
5
+ import * as Alert from './ui/alert/index.js';
5
6
  import { t } from '@svadmin/core/i18n';
6
7
 
7
- let { children }: { children: Snippet } = $props();
8
+ let {
9
+ children,
10
+ fallback,
11
+ }: {
12
+ children: Snippet;
13
+ /** Optional custom error UI snippet */
14
+ fallback?: Snippet<[{ error: Error; reset: () => void }]>;
15
+ } = $props();
8
16
 
9
- let error = $state<Error | null>(null);
10
-
11
- function handleError(e: ErrorEvent) {
12
- error = e.error instanceof Error ? e.error : new Error(String(e.error));
13
- e.preventDefault();
14
- }
15
-
16
- function handleRejection(e: PromiseRejectionEvent) {
17
- error = e.reason instanceof Error ? e.reason : new Error(String(e.reason));
18
- e.preventDefault();
19
- }
20
-
21
- $effect(() => {
22
- window.addEventListener('error', handleError);
23
- window.addEventListener('unhandledrejection', handleRejection);
24
- return () => {
25
- window.removeEventListener('error', handleError);
26
- window.removeEventListener('unhandledrejection', handleRejection);
27
- };
28
- });
29
-
30
- function handleRetry() {
31
- error = null;
32
- window.location.reload();
17
+ function handleError(error: unknown) {
18
+ console.error('[svadmin] Uncaught error:', error);
33
19
  }
34
20
  </script>
35
21
 
36
- {#if error}
37
- <div class="flex h-screen items-center justify-center bg-background">
38
- <div class="max-w-md text-center space-y-4">
39
- <div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
40
- <AlertTriangle class="h-8 w-8 text-destructive" />
41
- </div>
42
- <h2 class="text-xl font-bold text-foreground">{t('common.error')}</h2>
43
- <p class="text-sm text-muted-foreground">{error.message}</p>
44
- <Button onclick={handleRetry}>
45
- {t('common.retry')}
46
- </Button>
47
- </div>
48
- </div>
49
- {:else}
22
+ <svelte:boundary onerror={handleError}>
50
23
  {@render children()}
51
- {/if}
24
+
25
+ {#snippet failed(error, reset)}
26
+ {#if fallback}
27
+ {@render fallback({ error: error instanceof Error ? error : new Error(String(error)), reset })}
28
+ {:else}
29
+ <div class="flex h-screen items-center justify-center bg-background">
30
+ <div class="max-w-md w-full px-4">
31
+ <Alert.Root variant="destructive" class="space-y-4">
32
+ <div class="flex items-start gap-3">
33
+ <AlertTriangle class="h-5 w-5 mt-0.5 shrink-0" />
34
+ <div class="space-y-2">
35
+ <Alert.Title>{t('common.error')}</Alert.Title>
36
+ <Alert.Description class="text-sm">
37
+ {error instanceof Error ? error.message : String(error)}
38
+ </Alert.Description>
39
+ </div>
40
+ </div>
41
+ <Button variant="outline" size="sm" onclick={reset} class="w-full">
42
+ {t('common.retry')}
43
+ </Button>
44
+ </Alert.Root>
45
+ </div>
46
+ </div>
47
+ {/if}
48
+ {/snippet}
49
+ </svelte:boundary>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
- import { AlertTriangle, AlertCircle } from 'lucide-svelte';
2
+ import { AlertTriangle, RefreshCw } from 'lucide-svelte';
3
3
  import { t } from '@svadmin/core';
4
4
  import { Button } from './ui/button';
5
+ import * as Alert from './ui/alert/index.js';
5
6
 
6
7
  let {
7
8
  error,
@@ -16,20 +17,20 @@
16
17
  }>();
17
18
  </script>
18
19
 
19
- <div class="flex flex-col items-center justify-center p-8 text-center min-h-[300px] bg-destructive/5 rounded-lg border border-destructive/20 text-destructive transition-all duration-300">
20
- <div class="h-16 w-16 rounded-full bg-destructive/10 flex items-center justify-center mb-4">
21
- <AlertTriangle class="h-8 w-8 text-destructive" />
22
- </div>
23
- <h3 class="text-lg font-semibold">{title}</h3>
24
- <p class="text-sm opacity-90 mt-2 max-w-md">
25
- {error?.message || description}
26
- </p>
27
- {#if retry}
28
- <div class="mt-6">
29
- <Button variant="outline" class="border-destructive/30 hover:bg-destructive/10 text-destructive" onclick={retry}>
30
- <AlertCircle class="mr-2 h-4 w-4" />
31
- {t('buttons.retry')}
32
- </Button>
33
- </div>
34
- {/if}
20
+ <div class="flex flex-col items-center justify-center p-8 text-center min-h-[300px]">
21
+ <Alert.Root variant="destructive" class="max-w-md text-left">
22
+ <AlertTriangle class="h-5 w-5" />
23
+ <Alert.Title>{title}</Alert.Title>
24
+ <Alert.Description>
25
+ {error?.message || description}
26
+ </Alert.Description>
27
+ {#if retry}
28
+ <div class="mt-4">
29
+ <Button variant="outline" size="sm" onclick={retry}>
30
+ <RefreshCw class="h-3 w-3" data-icon="inline-start" />
31
+ {t('buttons.retry')}
32
+ </Button>
33
+ </div>
34
+ {/if}
35
+ </Alert.Root>
35
36
  </div>