@svadmin/ui 0.0.6 → 0.4.1
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/package.json +8 -3
- package/src/actions.ts +110 -0
- package/src/components/AdminApp.svelte +8 -4
- package/src/components/Authenticated.svelte +38 -13
- package/src/components/AutoForm.svelte +163 -74
- package/src/components/AutoSaveIndicator.svelte +35 -0
- package/src/components/AutoTable.svelte +476 -183
- package/src/components/Breadcrumbs.svelte +22 -17
- package/src/components/CatchAllNavigate.svelte +21 -0
- package/src/components/ComboboxField.svelte +122 -0
- package/src/components/CommandPalette.svelte +93 -0
- package/src/components/ConfigErrorScreen.svelte +53 -151
- package/src/components/ConfirmDialog.svelte +25 -17
- package/src/components/CreatePage.svelte +43 -0
- package/src/components/DevTools.svelte +99 -230
- package/src/components/DrawerForm.svelte +19 -10
- package/src/components/EditPage.svelte +71 -0
- package/src/components/EmptyState.svelte +21 -31
- package/src/components/ErrorBoundary.svelte +38 -40
- package/src/components/ErrorComponent.svelte +36 -0
- package/src/components/FieldRenderer.svelte +240 -75
- package/src/components/ForgotPasswordPage.svelte +20 -111
- package/src/components/Header.svelte +46 -57
- package/src/components/InferencerPanel.svelte +136 -110
- package/src/components/InfiniteList.svelte +93 -0
- package/src/components/Layout.svelte +85 -22
- package/src/components/ListPage.svelte +71 -0
- package/src/components/LiveIndicator.svelte +58 -0
- package/src/components/LoginPage.svelte +33 -154
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/NavigateToResource.svelte +20 -0
- package/src/components/PageSkeleton.svelte +71 -0
- package/src/components/PasswordInput.svelte +95 -0
- package/src/components/RegisterPage.svelte +35 -164
- package/src/components/ShowPage.svelte +35 -36
- package/src/components/Sidebar.svelte +212 -81
- package/src/components/StatsCard.svelte +12 -3
- package/src/components/StepsForm.svelte +139 -0
- package/src/components/ThemedTitle.svelte +32 -0
- package/src/components/Toast.svelte +15 -44
- package/src/components/TooltipButton.svelte +52 -0
- package/src/components/UndoableNotification.svelte +28 -96
- package/src/components/UpdatePasswordPage.svelte +32 -160
- package/src/components/buttons/CloneButton.svelte +1 -1
- package/src/components/buttons/CreateButton.svelte +1 -1
- package/src/components/buttons/DeleteButton.svelte +3 -4
- package/src/components/buttons/EditButton.svelte +1 -1
- package/src/components/buttons/ImportButton.svelte +29 -7
- package/src/components/buttons/RefreshButton.svelte +11 -5
- package/src/components/fieldComponentMap.ts +79 -0
- package/src/components/fields/BooleanField.svelte +13 -0
- package/src/components/fields/DateField.svelte +6 -0
- package/src/components/fields/EmailField.svelte +9 -0
- package/src/components/fields/FileField.svelte +14 -0
- package/src/components/fields/ImageField.svelte +9 -0
- package/src/components/fields/JsonField.svelte +72 -0
- package/src/components/fields/MarkdownField.svelte +9 -0
- package/src/components/fields/MultiSelectField.svelte +20 -0
- package/src/components/fields/NumberField.svelte +6 -0
- package/src/components/fields/RelationField.svelte +24 -0
- package/src/components/fields/RichTextField.svelte +35 -0
- package/src/components/fields/SelectField.svelte +14 -0
- package/src/components/fields/TagField.svelte +12 -0
- package/src/components/fields/TextField.svelte +11 -0
- package/src/components/fields/UrlField.svelte +14 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
- package/src/components/ui/alert-dialog/index.ts +20 -0
- package/src/components/ui/avatar/avatar.svelte +2 -1
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
- package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/collapsible/index.ts +7 -0
- package/src/components/ui/command/command-dialog.svelte +31 -0
- package/src/components/ui/command/index.ts +2 -0
- package/src/components/ui/context-menu/index.ts +11 -0
- package/src/components/ui/drawer/drawer-content.svelte +21 -0
- package/src/components/ui/drawer/drawer-description.svelte +11 -0
- package/src/components/ui/drawer/drawer-footer.svelte +11 -0
- package/src/components/ui/drawer/drawer-header.svelte +11 -0
- package/src/components/ui/drawer/drawer-title.svelte +11 -0
- package/src/components/ui/drawer/drawer.svelte +18 -0
- package/src/components/ui/drawer/index.ts +6 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
- package/src/components/ui/dropdown-menu/index.ts +6 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/label/label.svelte +18 -0
- package/src/components/ui/pagination/index.ts +7 -0
- package/src/components/ui/pagination/pagination-content.svelte +11 -0
- package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
- package/src/components/ui/pagination/pagination-item.svelte +11 -0
- package/src/components/ui/pagination/pagination-link.svelte +26 -0
- package/src/components/ui/pagination/pagination-next.svelte +22 -0
- package/src/components/ui/pagination/pagination-previous.svelte +22 -0
- package/src/components/ui/pagination/pagination.svelte +11 -0
- package/src/components/ui/popover/index.ts +12 -0
- package/src/components/ui/popover/popover-content.svelte +25 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/progress/progress.svelte +22 -0
- package/src/components/ui/scroll-area/index.ts +1 -0
- package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
- package/src/components/ui/sheet/index.ts +15 -1
- package/src/components/ui/sheet/sheet-content.svelte +28 -0
- package/src/components/ui/sheet/sheet-description.svelte +22 -0
- package/src/components/ui/sheet/sheet-footer.svelte +22 -0
- package/src/components/ui/sheet/sheet-header.svelte +22 -0
- package/src/components/ui/sheet/sheet-title.svelte +22 -0
- package/src/index.ts +52 -1
- package/src/router-state.svelte.ts +16 -9
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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((
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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="
|
|
44
|
-
<
|
|
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
|
-
</
|
|
51
|
-
<
|
|
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
|
-
</
|
|
67
|
+
</TooltipButton>
|
|
54
68
|
</div>
|
|
55
69
|
</div>
|
|
56
70
|
|
|
57
71
|
{#if !collapsed}
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
State
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<div class="
|
|
101
|
-
<
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
</
|
|
106
|
-
|
|
129
|
+
</div>
|
|
130
|
+
</ScrollArea>
|
|
131
|
+
</Tabs.Content>
|
|
132
|
+
<Tabs.Content value="inferencer" class="p-0">
|
|
107
133
|
<InferencerPanel />
|
|
108
|
-
|
|
109
|
-
</
|
|
134
|
+
</Tabs.Content>
|
|
135
|
+
</Tabs.Root>
|
|
110
136
|
{/if}
|
|
111
137
|
</div>
|
|
112
138
|
{:else}
|
|
113
|
-
<
|
|
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
|
-
</
|
|
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 {
|
|
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
|
-
|
|
18
|
-
<Sheet
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
{mode === 'create' ?
|
|
22
|
-
</
|
|
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
|
-
|
|
34
|
+
</Sheet.Content>
|
|
35
|
+
</Sheet.Root>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getResource, useDelete } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import PageHeader from './PageHeader.svelte';
|
|
7
|
+
import AutoForm from './AutoForm.svelte';
|
|
8
|
+
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
9
|
+
import { ArrowLeft, Trash2 } from 'lucide-svelte';
|
|
10
|
+
import { Button } from './ui/button/index.js';
|
|
11
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
resourceName: string;
|
|
15
|
+
id: string | number;
|
|
16
|
+
title?: string;
|
|
17
|
+
canDelete?: boolean;
|
|
18
|
+
headerActions?: Snippet;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
resourceName,
|
|
24
|
+
id,
|
|
25
|
+
title,
|
|
26
|
+
canDelete,
|
|
27
|
+
headerActions,
|
|
28
|
+
class: className = '',
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const resource = getResource(resourceName);
|
|
32
|
+
const pageTitle = title ?? `${t('common.edit')} ${resource.label} #${id}`;
|
|
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
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div class="space-y-6 {className}">
|
|
48
|
+
<PageHeader title={pageTitle}>
|
|
49
|
+
{#snippet actions()}
|
|
50
|
+
<TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
|
|
51
|
+
<ArrowLeft class="h-5 w-5" />
|
|
52
|
+
</TooltipButton>
|
|
53
|
+
{#if showDelete}
|
|
54
|
+
<TooltipButton tooltip={t('common.delete')} variant="destructive" size="sm" onclick={() => { confirmOpen = true; }}>
|
|
55
|
+
<Trash2 class="h-4 w-4" />
|
|
56
|
+
</TooltipButton>
|
|
57
|
+
{/if}
|
|
58
|
+
{#if headerActions}
|
|
59
|
+
{@render headerActions()}
|
|
60
|
+
{/if}
|
|
61
|
+
{/snippet}
|
|
62
|
+
</PageHeader>
|
|
63
|
+
|
|
64
|
+
<AutoForm {resourceName} mode="edit" {id} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<ConfirmDialog
|
|
68
|
+
bind:open={confirmOpen}
|
|
69
|
+
title={t('common.deleteConfirm')}
|
|
70
|
+
onConfirm={handleDelete}
|
|
71
|
+
/>
|
|
@@ -1,41 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { InboxIcon } from 'lucide-svelte';
|
|
2
|
+
import { fade, scale } from 'svelte/transition';
|
|
3
|
+
import { PackageOpen } from 'lucide-svelte';
|
|
4
|
+
import { t } from '@svadmin/core';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
let {
|
|
7
|
+
title = t('empty.title'),
|
|
8
|
+
description = t('empty.description'),
|
|
9
|
+
children,
|
|
10
|
+
} = $props<{
|
|
9
11
|
title?: string;
|
|
10
12
|
description?: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let {
|
|
16
|
-
icon: Icon = InboxIcon,
|
|
17
|
-
title,
|
|
18
|
-
description,
|
|
19
|
-
action,
|
|
20
|
-
class: className = '',
|
|
21
|
-
}: Props = $props();
|
|
13
|
+
children?: import('svelte').Snippet;
|
|
14
|
+
}>();
|
|
22
15
|
</script>
|
|
23
16
|
|
|
24
|
-
<div
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
>
|
|
21
|
+
<div class="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
22
|
+
<PackageOpen class="h-10 w-10 text-muted-foreground" />
|
|
27
23
|
</div>
|
|
28
|
-
<h3 class="text-lg font-semibold
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{description}
|
|
34
|
-
</p>
|
|
35
|
-
{/if}
|
|
36
|
-
{#if action}
|
|
37
|
-
<div class="mt-4">
|
|
38
|
-
{@render action()}
|
|
24
|
+
<h3 class="text-lg font-semibold tracking-tight">{title}</h3>
|
|
25
|
+
<p class="text-sm text-muted-foreground mt-2 max-w-sm">{description}</p>
|
|
26
|
+
{#if children}
|
|
27
|
+
<div class="mt-6">
|
|
28
|
+
{@render children()}
|
|
39
29
|
</div>
|
|
40
30
|
{/if}
|
|
41
31
|
</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 {
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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>
|