@svadmin/ui 0.34.9 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.css +372 -0
- package/dist/component-registry.svelte.d.ts +1 -0
- package/dist/component-registry.svelte.js +1 -0
- package/dist/components/AICommandBar.svelte +3 -3
- package/dist/components/AboutSettings.svelte +2 -2
- package/dist/components/AdminApp.svelte +96 -18
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/AnomalyBadge.svelte +1 -1
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AppearanceSettings.svelte +5 -6
- package/dist/components/AuditLogDrawer.svelte +1 -1
- package/dist/components/AuditLogViewer.svelte +1 -1
- package/dist/components/AutoForm.svelte +50 -44
- package/dist/components/AutoTable.svelte +44 -39
- package/dist/components/Breadcrumbs.svelte +1 -0
- package/dist/components/ChatDialog.svelte +5 -4
- package/dist/components/ComboboxField.svelte +3 -3
- package/dist/components/CommandPalette.svelte +3 -3
- package/dist/components/CopilotPanel.svelte +2 -2
- package/dist/components/CreatePage.svelte +0 -1
- package/dist/components/DevTools.svelte +4 -3
- package/dist/components/DraggableHeader.svelte +2 -1
- package/dist/components/EditPage.svelte +1 -1
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +9 -8
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +7 -7
- package/dist/components/InferencerPanel.svelte +4 -3
- package/dist/components/InfiniteList.svelte +3 -4
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/KeyboardShortcuts.svelte +3 -3
- package/dist/components/Layout.svelte +13 -13
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/MarkdownRenderer.svelte +3 -5
- package/dist/components/ModalForm.svelte +1 -3
- package/dist/components/NotificationsSettings.svelte +115 -0
- package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +9 -7
- package/dist/components/PageSkeleton.svelte +4 -4
- package/dist/components/PasswordInput.svelte +1 -2
- package/dist/components/PermissionMatrix.svelte +5 -5
- package/dist/components/ProfilePage.svelte +1 -1
- package/dist/components/RegisterPage.svelte +103 -65
- package/dist/components/ResourceOperationsPage.svelte +153 -0
- package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
- package/dist/components/RolesSettings.svelte +1 -2
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +121 -55
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +13 -15
- package/dist/components/Sidebar.svelte +107 -58
- package/dist/components/SidebarItem.svelte +38 -14
- package/dist/components/StepsForm.svelte +1 -1
- package/dist/components/TaskList.svelte +1 -1
- package/dist/components/TaskQueueDrawer.svelte +2 -1
- package/dist/components/TenantSwitcher.svelte +1 -2
- package/dist/components/Toast.svelte +1 -0
- package/dist/components/TooltipButton.svelte +1 -0
- package/dist/components/TwoFactorAuthPage.svelte +247 -0
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
- package/dist/components/UpdatePasswordPage.svelte +86 -48
- package/dist/components/VirtualTable.svelte +1 -1
- package/dist/components/VoiceInput.svelte +1 -0
- package/dist/components/account/CompanyProfilePage.svelte +99 -0
- package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
- package/dist/components/account/GetStartedPage.svelte +123 -0
- package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
- package/dist/components/account/ImportMembersPage.svelte +143 -0
- package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
- package/dist/components/account/MembersStarterPage.svelte +104 -0
- package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
- package/dist/components/account/SecurityLogPage.svelte +108 -0
- package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
- package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsPlainPage.svelte +81 -0
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsSidebarPage.svelte +98 -0
- package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
- package/dist/components/account/TeamMembersPage.svelte +98 -0
- package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
- package/dist/components/buttons/ImportButton.svelte +1 -1
- package/dist/components/buttons/ListButton.svelte +1 -1
- package/dist/components/charts/BarChart.svelte +1 -1
- package/dist/components/charts/LineChart.svelte +3 -3
- package/dist/components/charts/PieChart.svelte +2 -2
- package/dist/components/fieldComponentMap.js +2 -1
- package/dist/components/fields/ArrayField.svelte +1 -1
- package/dist/components/fields/MarkdownField.svelte +1 -0
- package/dist/components/fields/MultiSelectField.svelte +1 -1
- package/dist/components/fields/TagField.svelte +1 -1
- package/dist/components/network/TeamCrewTablePage.svelte +122 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/components/ui/alert/index.js +1 -0
- package/dist/components/ui/badge/index.js +1 -0
- package/dist/components/ui/button/button.svelte +1 -0
- package/dist/components/ui/button/index.js +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +1 -1
- package/dist/components/ui/sheet/sheet.svelte +1 -0
- package/dist/components/ui/tabs/tabs.svelte +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/router-state.svelte.js +17 -0
- package/package.json +13 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Badge } from './ui/badge/index.js';
|
|
3
|
-
import { TrendingUp, TrendingDown
|
|
3
|
+
import { TrendingUp, TrendingDown } from '@lucide/svelte';
|
|
4
4
|
import TooltipButton from './TooltipButton.svelte';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import { AlertCircle, Calendar, Check, Copy, Key, Plus, ShieldCheck, Trash2 } from '@lucide/svelte';
|
|
4
|
+
import * as Card from './ui/card/index.js';
|
|
5
|
+
import { Badge } from './ui/badge/index.js';
|
|
6
|
+
import { Button } from './ui/button/index.js';
|
|
7
|
+
import { Checkbox } from './ui/checkbox/index.js';
|
|
8
|
+
import { Input } from './ui/input/index.js';
|
|
9
|
+
import { Label } from './ui/label/index.js';
|
|
10
|
+
|
|
11
|
+
interface ApiKey {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
maskedToken: string;
|
|
15
|
+
created: string;
|
|
16
|
+
permissions: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let keys = $state<ApiKey[]>([
|
|
20
|
+
{ id: 'prod-sync', name: 'Production Sync Service', maskedToken: 'sv_demo_prod_••••_4d3e', created: '2026-03-10', permissions: ['Read', 'Write'] },
|
|
21
|
+
{ id: 'reporting', name: 'Internal Reporting Tool', maskedToken: 'sv_demo_report_••••_9e8f', created: '2026-05-18', permissions: ['Read'] },
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
let newKeyName = $state('');
|
|
25
|
+
let newKeyPermissions = $state({ Read: true, Write: false, Delete: false, Admin: false });
|
|
26
|
+
let generatedToken = $state('');
|
|
27
|
+
let showCreatedToken = $state(false);
|
|
28
|
+
let copiedKeyId = $state<string | null>(null);
|
|
29
|
+
|
|
30
|
+
function generateDemoToken() {
|
|
31
|
+
const randomHex = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
32
|
+
return `sv_demo_${randomHex}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleGenerateKey(event: SubmitEvent) {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
if (!newKeyName.trim()) return;
|
|
38
|
+
|
|
39
|
+
const token = generateDemoToken();
|
|
40
|
+
const permissions = Object.entries(newKeyPermissions)
|
|
41
|
+
.filter(([, enabled]) => enabled)
|
|
42
|
+
.map(([name]) => name);
|
|
43
|
+
|
|
44
|
+
keys = [
|
|
45
|
+
...keys,
|
|
46
|
+
{
|
|
47
|
+
id: Date.now().toString(),
|
|
48
|
+
name: newKeyName.trim(),
|
|
49
|
+
maskedToken: `${token.slice(0, 14)}••••${token.slice(-4)}`,
|
|
50
|
+
created: new Date().toISOString().split('T')[0],
|
|
51
|
+
permissions,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
generatedToken = token;
|
|
55
|
+
showCreatedToken = true;
|
|
56
|
+
newKeyName = '';
|
|
57
|
+
newKeyPermissions = { Read: true, Write: false, Delete: false, Admin: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deleteKey(id: string) {
|
|
61
|
+
keys = keys.filter((key) => key.id !== id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function copyToken(token: string, id: string) {
|
|
65
|
+
try {
|
|
66
|
+
await navigator.clipboard.writeText(token);
|
|
67
|
+
copiedKeyId = id;
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
if (copiedKeyId === id) copiedKeyId = null;
|
|
70
|
+
}, 2000);
|
|
71
|
+
} catch {
|
|
72
|
+
copiedKeyId = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<div class="space-y-6">
|
|
78
|
+
<div>
|
|
79
|
+
<h2 class="text-xl font-semibold text-foreground">{t('settings.api')}</h2>
|
|
80
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('settings.apiDescription')}</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="grid gap-6 lg:grid-cols-3">
|
|
84
|
+
<Card.Card class="lg:col-span-1">
|
|
85
|
+
<Card.CardHeader>
|
|
86
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
87
|
+
<Plus class="h-4 w-4" />
|
|
88
|
+
{t('api.generateTitle')}
|
|
89
|
+
</Card.CardTitle>
|
|
90
|
+
<Card.CardDescription>{t('api.generateDesc')}</Card.CardDescription>
|
|
91
|
+
</Card.CardHeader>
|
|
92
|
+
<Card.CardContent>
|
|
93
|
+
<form onsubmit={handleGenerateKey} class="space-y-4">
|
|
94
|
+
<div class="space-y-2">
|
|
95
|
+
<Label for="api-key-name">{t('api.keyName')}</Label>
|
|
96
|
+
<Input id="api-key-name" placeholder="CI deployment token" bind:value={newKeyName} required />
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="space-y-3">
|
|
100
|
+
<Label>{t('api.permissions')}</Label>
|
|
101
|
+
{#each Object.keys(newKeyPermissions) as permission (permission)}
|
|
102
|
+
<div class="flex items-center gap-2">
|
|
103
|
+
<Checkbox id={`perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} />
|
|
104
|
+
<Label for={`perm-${permission}`} class="cursor-pointer text-sm font-normal">{permission}</Label>
|
|
105
|
+
</div>
|
|
106
|
+
{/each}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<Button type="submit" class="w-full">{t('api.generateButton')}</Button>
|
|
110
|
+
</form>
|
|
111
|
+
</Card.CardContent>
|
|
112
|
+
</Card.Card>
|
|
113
|
+
|
|
114
|
+
<Card.Card class="lg:col-span-2">
|
|
115
|
+
<Card.CardHeader>
|
|
116
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
117
|
+
<Key class="h-4 w-4 text-muted-foreground" />
|
|
118
|
+
{t('api.activeKeysTitle')}
|
|
119
|
+
</Card.CardTitle>
|
|
120
|
+
<Card.CardDescription>{t('api.activeKeysDesc')}</Card.CardDescription>
|
|
121
|
+
</Card.CardHeader>
|
|
122
|
+
<Card.CardContent>
|
|
123
|
+
{#if showCreatedToken}
|
|
124
|
+
<div class="mb-6 space-y-3 rounded-lg border border-green-500/20 bg-green-500/5 p-4">
|
|
125
|
+
<div class="flex items-start gap-2 text-sm text-green-700 dark:text-green-300">
|
|
126
|
+
<ShieldCheck class="mt-0.5 h-5 w-5 shrink-0" />
|
|
127
|
+
<div>
|
|
128
|
+
<h4 class="font-semibold">{t('api.successTitle')}</h4>
|
|
129
|
+
<p class="mt-0.5 text-xs">{t('api.successHint')}</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="flex items-center gap-2">
|
|
133
|
+
<Input readonly value={generatedToken} class="h-9 font-mono text-xs" />
|
|
134
|
+
<Button size="sm" onclick={() => copyToken(generatedToken, 'new-token')}>
|
|
135
|
+
{#if copiedKeyId === 'new-token'}
|
|
136
|
+
<Check class="h-4 w-4" />
|
|
137
|
+
{:else}
|
|
138
|
+
<Copy class="h-4 w-4" />
|
|
139
|
+
{/if}
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="flex justify-end">
|
|
143
|
+
<Button variant="ghost" size="sm" onclick={() => { showCreatedToken = false; generatedToken = ''; }}>{t('common.close')}</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
<div class="divide-y">
|
|
149
|
+
{#each keys as key (key.id)}
|
|
150
|
+
<div class="space-y-3 py-4 first:pt-0 last:pb-0">
|
|
151
|
+
<div class="flex items-start justify-between gap-4">
|
|
152
|
+
<div class="min-w-0 space-y-1">
|
|
153
|
+
<h4 class="text-sm font-semibold text-foreground">{key.name}</h4>
|
|
154
|
+
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
155
|
+
<span class="font-mono">{key.maskedToken}</span>
|
|
156
|
+
<span class="flex items-center gap-1">
|
|
157
|
+
<Calendar class="h-3 w-3" />
|
|
158
|
+
{key.created}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
163
|
+
<Button variant="ghost" size="icon-sm" onclick={() => copyToken(key.maskedToken, key.id)} title={t('common.copy')}>
|
|
164
|
+
{#if copiedKeyId === key.id}
|
|
165
|
+
<Check class="h-4 w-4 text-green-500" />
|
|
166
|
+
{:else}
|
|
167
|
+
<Copy class="h-4 w-4" />
|
|
168
|
+
{/if}
|
|
169
|
+
</Button>
|
|
170
|
+
<Button variant="ghost" size="icon-sm" onclick={() => deleteKey(key.id)} title={t('common.delete')}>
|
|
171
|
+
<Trash2 class="h-4 w-4" />
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="flex flex-wrap gap-1">
|
|
176
|
+
{#each key.permissions as permission (permission)}
|
|
177
|
+
<Badge variant="secondary" class="px-2 py-0 text-[10px]">{permission}</Badge>
|
|
178
|
+
{/each}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
{:else}
|
|
182
|
+
<div class="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted-foreground">
|
|
183
|
+
<AlertCircle class="h-8 w-8 opacity-60" />
|
|
184
|
+
<span>{t('api.noKeys')}</span>
|
|
185
|
+
</div>
|
|
186
|
+
{/each}
|
|
187
|
+
</div>
|
|
188
|
+
</Card.CardContent>
|
|
189
|
+
</Card.Card>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
|
-
import { getTheme, setTheme,
|
|
3
|
+
import { getTheme, setTheme, getColorTheme, setColorTheme, getColorPresets } from '@svadmin/core';
|
|
4
4
|
import { getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
5
5
|
import * as Card from './ui/card/index.js';
|
|
6
|
-
import { Button } from './ui/button/index.js';
|
|
7
6
|
import { Sun, Moon, Monitor, Check } from '@lucide/svelte';
|
|
8
7
|
|
|
9
8
|
// Theme mode
|
|
@@ -67,7 +66,7 @@
|
|
|
67
66
|
</Card.CardHeader>
|
|
68
67
|
<Card.CardContent>
|
|
69
68
|
<div class="grid grid-cols-3 gap-3">
|
|
70
|
-
{#each themeOptions as opt}
|
|
69
|
+
{#each themeOptions as opt, _i (_i)}
|
|
71
70
|
{@const active = currentTheme === opt.value}
|
|
72
71
|
<button
|
|
73
72
|
class="relative flex flex-col items-center gap-2 rounded-xl border-2 p-4 transition-all duration-200
|
|
@@ -101,7 +100,7 @@
|
|
|
101
100
|
</Card.CardHeader>
|
|
102
101
|
<Card.CardContent>
|
|
103
102
|
<div class="flex flex-wrap gap-3">
|
|
104
|
-
{#each presets as preset}
|
|
103
|
+
{#each presets as preset, _i (_i)}
|
|
105
104
|
{@const active = currentColor === preset.name}
|
|
106
105
|
<button
|
|
107
106
|
class="group relative h-9 w-9 rounded-full transition-all duration-200 hover:scale-110
|
|
@@ -137,7 +136,7 @@
|
|
|
137
136
|
value={currentLocale}
|
|
138
137
|
onchange={(e) => setLocale((e.target as HTMLSelectElement).value)}
|
|
139
138
|
>
|
|
140
|
-
{#each availableLocales as loc}
|
|
139
|
+
{#each availableLocales as loc, _i (_i)}
|
|
141
140
|
<option value={loc}>{localeNames[loc] ?? loc}</option>
|
|
142
141
|
{/each}
|
|
143
142
|
</select>
|
|
@@ -172,7 +171,7 @@
|
|
|
172
171
|
<p class="text-sm font-medium text-foreground">{t('settings.defaultPageSize')}</p>
|
|
173
172
|
</div>
|
|
174
173
|
<div class="flex rounded-lg border border-input overflow-hidden">
|
|
175
|
-
{#each pageSizeOptions as size}
|
|
174
|
+
{#each pageSizeOptions as size, _i (_i)}
|
|
176
175
|
<button
|
|
177
176
|
class="px-3 py-1.5 text-xs font-mono font-medium transition-colors
|
|
178
177
|
{pageSize === size ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
{:else}
|
|
70
70
|
<div class="relative border-l border-border ml-3 pl-6 space-y-8">
|
|
71
|
-
{#each logs as log}
|
|
71
|
+
{#each logs as log, _i (_i)}
|
|
72
72
|
<div class="relative">
|
|
73
73
|
<span class="absolute -left-7 top-1 flex h-2 w-2 items-center justify-center rounded-full ring-4 ring-background {getActionColor(log.action)}"></span>
|
|
74
74
|
<div class="flex flex-col gap-1.5">
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
</Table.Row>
|
|
131
131
|
</Table.Header>
|
|
132
132
|
<Table.Body>
|
|
133
|
-
{#each filteredLogs as log}
|
|
133
|
+
{#each filteredLogs as log, _i (_i)}
|
|
134
134
|
<Table.Row class="border-0 even:bg-muted/20 hover:bg-muted/40 transition-colors">
|
|
135
135
|
<Table.Cell class="text-xs text-muted-foreground font-mono">
|
|
136
136
|
{formatDate(log.createdAt)}
|
|
@@ -44,14 +44,17 @@
|
|
|
44
44
|
const hasGroups = $derived(formFields.some(f => f.group));
|
|
45
45
|
const groups = $derived((() => {
|
|
46
46
|
if (!hasGroups) return [];
|
|
47
|
-
const
|
|
48
|
-
const map = new Map<string, FieldDefinition[]>();
|
|
47
|
+
const result: Array<{ name: string; fields: FieldDefinition[] }> = [];
|
|
49
48
|
for (const f of formFields) {
|
|
50
49
|
const g = f.group ?? '';
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
let group = result.find(item => item.name === g);
|
|
51
|
+
if (!group) {
|
|
52
|
+
group = { name: g, fields: [] };
|
|
53
|
+
result.push(group);
|
|
54
|
+
}
|
|
55
|
+
group.fields.push(f);
|
|
53
56
|
}
|
|
54
|
-
return
|
|
57
|
+
return result;
|
|
55
58
|
})());
|
|
56
59
|
|
|
57
60
|
// ─── Default values from field metadata ───────────────────────────
|
|
@@ -75,18 +78,6 @@
|
|
|
75
78
|
|
|
76
79
|
const validator = $derived(deriveValidator(formFields));
|
|
77
80
|
|
|
78
|
-
$effect(() => {
|
|
79
|
-
if (mode !== 'create') return;
|
|
80
|
-
if (!form.isTainted()) {
|
|
81
|
-
for (const key in defaults) {
|
|
82
|
-
if (form.values[key] !== defaults[key]) {
|
|
83
|
-
form.setFieldValue(key, defaults[key]);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
form.untaint();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
81
|
// ─── useForm: single source of truth for values, errors, tainted ──
|
|
91
82
|
const form = useForm({
|
|
92
83
|
get resource() { return resourceName; },
|
|
@@ -144,24 +135,38 @@
|
|
|
144
135
|
}
|
|
145
136
|
</script>
|
|
146
137
|
|
|
147
|
-
<div class="space-y-6">
|
|
148
|
-
<div class="
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
138
|
+
<div class="space-y-6" data-svadmin-form-shell>
|
|
139
|
+
<div class="rounded-3xl border border-border/70 bg-card/90 p-5 shadow-sm shadow-slate-900/[0.03] backdrop-blur" data-svadmin-form-header>
|
|
140
|
+
<div class="flex flex-wrap items-center gap-4">
|
|
141
|
+
<TooltipButton
|
|
142
|
+
tooltip={t('common.back')}
|
|
143
|
+
onclick={() => guardNavigate(() => navigate(`/${resourceName}`))}
|
|
144
|
+
class="h-10 w-10 rounded-xl border border-border/70 bg-background/80"
|
|
145
|
+
>
|
|
146
|
+
<ArrowLeft class="h-5 w-5" />
|
|
147
|
+
</TooltipButton>
|
|
148
|
+
<div class="min-w-0 flex-1">
|
|
149
|
+
<h1 class="truncate text-2xl font-semibold tracking-tight text-foreground">{pageTitle}</h1>
|
|
150
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
151
|
+
<Badge variant="secondary" class="rounded-full px-2.5 py-0.5 text-[0.6875rem] font-semibold">
|
|
152
|
+
{mode === 'create' ? t('common.create') : mode === 'show' ? t('common.detail') : t('common.edit')}
|
|
153
|
+
</Badge>
|
|
154
|
+
<p class="text-sm text-muted-foreground">{resource.label}</p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
{#if headerContent}
|
|
158
|
+
{@render headerContent()}
|
|
159
|
+
{/if}
|
|
160
|
+
{#if form.isTainted()}
|
|
161
|
+
<Badge variant="outline" class="border-warning/30 bg-warning/10 text-warning-foreground">{t('common.unsaved')}</Badge>
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
159
164
|
</div>
|
|
160
165
|
|
|
161
166
|
{#if form.loading}
|
|
162
|
-
<div class="max-w-
|
|
163
|
-
<div class="
|
|
164
|
-
{#each Array(4) as _}
|
|
167
|
+
<div class="max-w-4xl space-y-6">
|
|
168
|
+
<div class="space-y-5 rounded-3xl border border-border/70 bg-card/90 p-6 shadow-sm shadow-slate-900/[0.03]">
|
|
169
|
+
{#each Array(4) as _, _i (_i)}
|
|
165
170
|
<div class="space-y-2">
|
|
166
171
|
<Skeleton class="h-4 w-24" />
|
|
167
172
|
<Skeleton class="h-10 w-full" />
|
|
@@ -170,7 +175,7 @@
|
|
|
170
175
|
</div>
|
|
171
176
|
</div>
|
|
172
177
|
{:else}
|
|
173
|
-
<form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-
|
|
178
|
+
<form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-5xl space-y-6" data-svadmin-form-settings>
|
|
174
179
|
{#if submitError}
|
|
175
180
|
<div transition:slide={{ duration: 300, axis: 'y' }} class="svadmin-shake">
|
|
176
181
|
<Alert.Root variant="destructive">
|
|
@@ -181,16 +186,16 @@
|
|
|
181
186
|
{/if}
|
|
182
187
|
|
|
183
188
|
{#if hasGroups}
|
|
184
|
-
{#each groups as group}
|
|
185
|
-
<Card.Root class="border-border/
|
|
189
|
+
{#each groups as group, _i (_i)}
|
|
190
|
+
<Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]" data-svadmin-form-card>
|
|
186
191
|
{#if group.name}
|
|
187
|
-
<Card.Header>
|
|
188
|
-
<Card.Title class="text-
|
|
192
|
+
<Card.Header class="border-b border-border/60 bg-muted/25 px-5 py-4 sm:px-6">
|
|
193
|
+
<Card.Title class="text-base font-semibold">{group.name}</Card.Title>
|
|
189
194
|
</Card.Header>
|
|
190
195
|
{/if}
|
|
191
|
-
<Card.Content class="
|
|
196
|
+
<Card.Content class="px-4 py-0 sm:px-6" data-svadmin-form-content>
|
|
192
197
|
{#each group.fields as field (field.key)}
|
|
193
|
-
<div class:border-destructive={!!form.errors[field.key]}>
|
|
198
|
+
<div data-svadmin-form-row class:border-destructive={!!form.errors[field.key]}>
|
|
194
199
|
{#if fieldRenderer}
|
|
195
200
|
{@render fieldRenderer({ field, value: form.values[field.key], onchange: (val: unknown) => form.setFieldValue(field.key, val) })}
|
|
196
201
|
{:else}
|
|
@@ -210,10 +215,10 @@
|
|
|
210
215
|
</Card.Root>
|
|
211
216
|
{/each}
|
|
212
217
|
{:else}
|
|
213
|
-
<Card.Root class="border-border/
|
|
214
|
-
<Card.Content class="
|
|
218
|
+
<Card.Root class="rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]" data-svadmin-form-card>
|
|
219
|
+
<Card.Content class="px-4 py-0 sm:px-6" data-svadmin-form-content>
|
|
215
220
|
{#each formFields as field (field.key)}
|
|
216
|
-
<div class:border-destructive={!!form.errors[field.key]}>
|
|
221
|
+
<div data-svadmin-form-row class:border-destructive={!!form.errors[field.key]}>
|
|
217
222
|
{#if fieldRenderer}
|
|
218
223
|
{@render fieldRenderer({ field, value: form.values[field.key], onchange: (val: unknown) => form.setFieldValue(field.key, val) })}
|
|
219
224
|
{:else}
|
|
@@ -233,11 +238,11 @@
|
|
|
233
238
|
</Card.Root>
|
|
234
239
|
{/if}
|
|
235
240
|
|
|
236
|
-
<div class="flex items-center gap-3">
|
|
241
|
+
<div class="sticky bottom-4 z-10 flex items-center gap-3 rounded-3xl border border-border/70 bg-card/90 p-3 shadow-lg shadow-slate-900/[0.05] backdrop-blur" data-svadmin-form-actions>
|
|
237
242
|
{#if formActions}
|
|
238
243
|
{@render formActions({ isLoading: form.submitting, onSubmit: handleSubmit })}
|
|
239
244
|
{:else if !isReadonly}
|
|
240
|
-
<Button type="submit" disabled={form.submitting}>
|
|
245
|
+
<Button type="submit" disabled={form.submitting} class="h-10 rounded-xl px-5 shadow-sm shadow-primary/15">
|
|
241
246
|
{#if form.submitting}
|
|
242
247
|
<Loader2 class="h-4 w-4 animate-spin" data-icon="inline-start" />
|
|
243
248
|
{:else}
|
|
@@ -248,6 +253,7 @@
|
|
|
248
253
|
<Button
|
|
249
254
|
type="button"
|
|
250
255
|
variant="outline"
|
|
256
|
+
class="h-10 rounded-xl"
|
|
251
257
|
onclick={() => guardNavigate(() => navigate(`/${resourceName}`))}
|
|
252
258
|
>
|
|
253
259
|
{t('common.cancel')}
|