@svadmin/ui 0.36.0 → 0.38.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 +76 -104
- package/dist/components/AdminApp.svelte +18 -14
- package/dist/components/AutoForm.svelte +32 -47
- package/dist/components/AutoTable.svelte +26 -31
- package/dist/components/CanAccess.svelte +3 -2
- package/dist/components/CanAccess.svelte.d.ts +1 -0
- package/dist/components/CreateInferencer.svelte +13 -0
- package/dist/components/CreateInferencer.svelte.d.ts +7 -0
- package/dist/components/EditInferencer.svelte +21 -0
- package/dist/components/EditInferencer.svelte.d.ts +9 -0
- package/dist/components/Header.svelte +6 -6
- package/dist/components/Layout.svelte +21 -11
- package/dist/components/ListInferencer.svelte +14 -0
- package/dist/components/ListInferencer.svelte.d.ts +7 -0
- package/dist/components/PageHeader.svelte +7 -9
- package/dist/components/ResourceInferencer.svelte +28 -0
- package/dist/components/ResourceInferencer.svelte.d.ts +7 -0
- package/dist/components/ResourceOperationsPage.svelte +168 -21
- package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -0
- package/dist/components/SettingsPage.svelte +76 -41
- package/dist/components/ShowInferencer.svelte +21 -0
- package/dist/components/ShowInferencer.svelte.d.ts +9 -0
- package/dist/components/ShowPage.svelte +11 -11
- package/dist/components/ShowPage.svelte.d.ts +1 -0
- package/dist/components/Sidebar.svelte +214 -300
- package/dist/components/Sidebar.svelte.d.ts +0 -1
- package/dist/components/SidebarItem.svelte +141 -108
- package/dist/components/account/TeamMembersPage.svelte +9 -3
- package/dist/components/buttons/CloneButton.svelte +10 -2
- package/dist/components/buttons/CloneButton.svelte.d.ts +2 -4
- package/dist/components/buttons/CreateButton.svelte +9 -2
- package/dist/components/buttons/CreateButton.svelte.d.ts +2 -4
- package/dist/components/buttons/DeleteButton.svelte +10 -2
- package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -4
- package/dist/components/buttons/EditButton.svelte +10 -2
- package/dist/components/buttons/EditButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ExportButton.svelte +9 -2
- package/dist/components/buttons/ExportButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ImportButton.svelte +9 -2
- package/dist/components/buttons/ImportButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ShowButton.svelte +10 -2
- package/dist/components/buttons/ShowButton.svelte.d.ts +2 -4
- package/dist/components/buttons/access-control.d.ts +7 -0
- package/dist/components/buttons/access-control.js +5 -0
- package/dist/components/ui/sheet/sheet.svelte +3 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/router-state.svelte.js +15 -0
- package/package.json +2 -2
|
@@ -25,6 +25,17 @@
|
|
|
25
25
|
badge?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
type WorkspaceStyle =
|
|
29
|
+
| 'inventory'
|
|
30
|
+
| 'operations'
|
|
31
|
+
| 'people'
|
|
32
|
+
| 'calendar'
|
|
33
|
+
| 'communications'
|
|
34
|
+
| 'crm'
|
|
35
|
+
| 'property'
|
|
36
|
+
| 'ai'
|
|
37
|
+
| 'store';
|
|
38
|
+
|
|
28
39
|
interface Props {
|
|
29
40
|
resourceName: string;
|
|
30
41
|
eyebrow: string;
|
|
@@ -39,6 +50,7 @@
|
|
|
39
50
|
tableDescription?: string;
|
|
40
51
|
emptyLanesText?: string;
|
|
41
52
|
highlightsLabel?: string;
|
|
53
|
+
workspaceStyle?: WorkspaceStyle;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
let {
|
|
@@ -55,11 +67,12 @@
|
|
|
55
67
|
tableDescription,
|
|
56
68
|
emptyLanesText = 'No grouped records yet.',
|
|
57
69
|
highlightsLabel = 'Focus queue',
|
|
70
|
+
workspaceStyle = 'operations',
|
|
58
71
|
}: Props = $props();
|
|
59
72
|
</script>
|
|
60
73
|
|
|
61
|
-
<div class="space-y-6" data-svadmin-resource-operations={resourceName}>
|
|
62
|
-
<section class="
|
|
74
|
+
<div class="space-y-6" data-svadmin-resource-operations={resourceName} data-svadmin-workspace-style={workspaceStyle}>
|
|
75
|
+
<section class="space-y-5 border-b border-border/60 pb-5" data-svadmin-ops-hero>
|
|
63
76
|
<div class="min-w-0 py-1">
|
|
64
77
|
<Badge>{eyebrow}</Badge>
|
|
65
78
|
<div class="flex items-start gap-4">
|
|
@@ -78,7 +91,7 @@
|
|
|
78
91
|
</Button>
|
|
79
92
|
</div>
|
|
80
93
|
|
|
81
|
-
<div class="grid gap-4 md:grid-cols-3">
|
|
94
|
+
<div class="grid min-w-0 gap-4 md:grid-cols-3">
|
|
82
95
|
{#each metrics as metric (metric.label)}
|
|
83
96
|
<Card.Root data-svadmin-metric-card>
|
|
84
97
|
<Card.Content class="p-5">
|
|
@@ -93,8 +106,8 @@
|
|
|
93
106
|
</div>
|
|
94
107
|
</section>
|
|
95
108
|
|
|
96
|
-
<div class="grid gap-4
|
|
97
|
-
<div class="
|
|
109
|
+
<div class="grid min-w-0 gap-4">
|
|
110
|
+
<div class="grid min-w-0 gap-4 {workspaceStyle !== 'ai' && workspaceStyle !== 'communications' ? 'xl:grid-cols-2' : ''}">
|
|
98
111
|
<Card.Root data-svadmin-panel-card>
|
|
99
112
|
<Card.Header>
|
|
100
113
|
<Card.Title>{tableLabel}</Card.Title>
|
|
@@ -120,27 +133,161 @@
|
|
|
120
133
|
</Card.Root>
|
|
121
134
|
|
|
122
135
|
{#if highlights.length > 0}
|
|
123
|
-
<Card.Root data-svadmin-panel-card>
|
|
136
|
+
<Card.Root data-svadmin-panel-card data-svadmin-workbench={workspaceStyle}>
|
|
124
137
|
<Card.Header>
|
|
125
138
|
<Card.Title>{highlightsLabel}</Card.Title>
|
|
126
139
|
</Card.Header>
|
|
127
|
-
<Card.Content
|
|
128
|
-
{#
|
|
129
|
-
<div class="
|
|
130
|
-
<div class="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
<Card.Content>
|
|
141
|
+
{#if workspaceStyle === 'ai'}
|
|
142
|
+
<div class="grid gap-3 lg:grid-cols-[0.9fr_1.1fr]">
|
|
143
|
+
<div class="space-y-2">
|
|
144
|
+
{#each lanes as lane (lane.label)}
|
|
145
|
+
<div class="rounded-lg border bg-muted/30 px-3 py-2">
|
|
146
|
+
<div class="flex items-center justify-between gap-3">
|
|
147
|
+
<span class="text-sm font-medium">{lane.label}</span>
|
|
148
|
+
<Badge variant="outline">{lane.value}</Badge>
|
|
149
|
+
</div>
|
|
150
|
+
{#if lane.hint}
|
|
151
|
+
<p class="mt-1 text-xs text-muted-foreground">{lane.hint}</p>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
<div class="space-y-3">
|
|
157
|
+
{#each highlights as item (item.title)}
|
|
158
|
+
<div class="rounded-2xl rounded-tl-sm border bg-card px-4 py-3">
|
|
159
|
+
<div class="flex items-center justify-between gap-3">
|
|
160
|
+
<p class="font-medium">{item.title}</p>
|
|
161
|
+
{#if item.badge}
|
|
162
|
+
<Badge>{item.badge}</Badge>
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
{#if item.description}
|
|
166
|
+
<p class="mt-2 text-sm text-muted-foreground">{item.description}</p>
|
|
167
|
+
{/if}
|
|
168
|
+
{#if item.meta}
|
|
169
|
+
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
{/each}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
{:else if workspaceStyle === 'communications'}
|
|
176
|
+
<div class="grid gap-4 lg:grid-cols-[0.82fr_1.18fr]">
|
|
177
|
+
<div class="space-y-2">
|
|
178
|
+
{#each lanes as lane (lane.label)}
|
|
179
|
+
<div class="flex items-center justify-between rounded-lg border bg-muted/25 px-3 py-2">
|
|
180
|
+
<div>
|
|
181
|
+
<p class="text-sm font-medium">{lane.label}</p>
|
|
182
|
+
{#if lane.hint}
|
|
183
|
+
<p class="text-xs text-muted-foreground">{lane.hint}</p>
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
<Badge variant="outline">{lane.value}</Badge>
|
|
187
|
+
</div>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
<div class="divide-y rounded-lg border">
|
|
191
|
+
{#each highlights as item (item.title)}
|
|
192
|
+
<div class="px-4 py-3">
|
|
193
|
+
<div class="flex items-center justify-between gap-3">
|
|
194
|
+
<p class="font-medium">{item.title}</p>
|
|
195
|
+
{#if item.badge}
|
|
196
|
+
<Badge>{item.badge}</Badge>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
199
|
+
{#if item.description}
|
|
200
|
+
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
201
|
+
{/if}
|
|
202
|
+
{#if item.meta}
|
|
203
|
+
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/each}
|
|
135
207
|
</div>
|
|
136
|
-
{#if item.description}
|
|
137
|
-
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
138
|
-
{/if}
|
|
139
|
-
{#if item.meta}
|
|
140
|
-
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
141
|
-
{/if}
|
|
142
208
|
</div>
|
|
143
|
-
{
|
|
209
|
+
{:else if workspaceStyle === 'crm'}
|
|
210
|
+
<div class="grid gap-3 md:grid-cols-3">
|
|
211
|
+
{#each lanes as lane (lane.label)}
|
|
212
|
+
<div class="rounded-lg border bg-muted/25 p-3">
|
|
213
|
+
<div class="flex items-center justify-between gap-3">
|
|
214
|
+
<p class="text-sm font-semibold">{lane.label}</p>
|
|
215
|
+
<Badge variant="outline">{lane.value}</Badge>
|
|
216
|
+
</div>
|
|
217
|
+
{#if lane.hint}
|
|
218
|
+
<p class="mt-2 text-xs text-muted-foreground">{lane.hint}</p>
|
|
219
|
+
{/if}
|
|
220
|
+
</div>
|
|
221
|
+
{/each}
|
|
222
|
+
</div>
|
|
223
|
+
<div class="mt-4 grid gap-3">
|
|
224
|
+
{#each highlights as item (item.title)}
|
|
225
|
+
<div class="rounded-lg border-l-4 border-l-primary bg-muted/20 px-4 py-3">
|
|
226
|
+
<div class="flex items-center justify-between gap-3">
|
|
227
|
+
<p class="font-medium">{item.title}</p>
|
|
228
|
+
{#if item.badge}
|
|
229
|
+
<Badge>{item.badge}</Badge>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
{#if item.description}
|
|
233
|
+
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
234
|
+
{/if}
|
|
235
|
+
{#if item.meta}
|
|
236
|
+
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
{/each}
|
|
240
|
+
</div>
|
|
241
|
+
{:else if workspaceStyle === 'property' || workspaceStyle === 'store'}
|
|
242
|
+
<div class="grid gap-3 md:grid-cols-2">
|
|
243
|
+
{#each highlights as item (item.title)}
|
|
244
|
+
<div class="rounded-xl border bg-gradient-to-br from-muted/45 to-background p-4">
|
|
245
|
+
<div class="flex items-center justify-between gap-3">
|
|
246
|
+
<p class="font-medium">{item.title}</p>
|
|
247
|
+
{#if item.badge}
|
|
248
|
+
<Badge>{item.badge}</Badge>
|
|
249
|
+
{/if}
|
|
250
|
+
</div>
|
|
251
|
+
{#if item.description}
|
|
252
|
+
<p class="mt-2 text-sm text-muted-foreground">{item.description}</p>
|
|
253
|
+
{/if}
|
|
254
|
+
{#if item.meta}
|
|
255
|
+
<p class="mt-3 text-xs font-semibold text-primary">{item.meta}</p>
|
|
256
|
+
{/if}
|
|
257
|
+
</div>
|
|
258
|
+
{/each}
|
|
259
|
+
{#each lanes as lane (lane.label)}
|
|
260
|
+
<div class="rounded-xl border bg-card p-4">
|
|
261
|
+
<div class="flex items-center justify-between gap-3">
|
|
262
|
+
<p class="font-medium">{lane.label}</p>
|
|
263
|
+
<Badge variant="outline">{lane.value}</Badge>
|
|
264
|
+
</div>
|
|
265
|
+
{#if lane.hint}
|
|
266
|
+
<p class="mt-2 text-xs text-muted-foreground">{lane.hint}</p>
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
{/each}
|
|
270
|
+
</div>
|
|
271
|
+
{:else}
|
|
272
|
+
<div class="space-y-3">
|
|
273
|
+
{#each highlights as item (item.title)}
|
|
274
|
+
<div class="py-3">
|
|
275
|
+
<div class="flex items-center justify-between gap-3">
|
|
276
|
+
<p class="font-medium">{item.title}</p>
|
|
277
|
+
{#if item.badge}
|
|
278
|
+
<Badge>{item.badge}</Badge>
|
|
279
|
+
{/if}
|
|
280
|
+
</div>
|
|
281
|
+
{#if item.description}
|
|
282
|
+
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
283
|
+
{/if}
|
|
284
|
+
{#if item.meta}
|
|
285
|
+
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
286
|
+
{/if}
|
|
287
|
+
</div>
|
|
288
|
+
{/each}
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
144
291
|
</Card.Content>
|
|
145
292
|
</Card.Root>
|
|
146
293
|
{/if}
|
|
@@ -15,6 +15,7 @@ interface HighlightItem {
|
|
|
15
15
|
meta?: string;
|
|
16
16
|
badge?: string;
|
|
17
17
|
}
|
|
18
|
+
type WorkspaceStyle = 'inventory' | 'operations' | 'people' | 'calendar' | 'communications' | 'crm' | 'property' | 'ai' | 'store';
|
|
18
19
|
interface Props {
|
|
19
20
|
resourceName: string;
|
|
20
21
|
eyebrow: string;
|
|
@@ -31,6 +32,7 @@ interface Props {
|
|
|
31
32
|
tableDescription?: string;
|
|
32
33
|
emptyLanesText?: string;
|
|
33
34
|
highlightsLabel?: string;
|
|
35
|
+
workspaceStyle?: WorkspaceStyle;
|
|
34
36
|
}
|
|
35
37
|
declare const ResourceOperationsPage: Component<Props, {}, "">;
|
|
36
38
|
type ResourceOperationsPage = ReturnType<typeof ResourceOperationsPage>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Component, Snippet } from "svelte";
|
|
3
3
|
import { t } from "@svadmin/core/i18n";
|
|
4
4
|
import { navigate } from "@svadmin/core/router";
|
|
5
|
-
import { User, Info, FileSearch, Lock, Puzzle } from "@lucide/svelte";
|
|
5
|
+
import { User, Palette, Info, Shield, FileSearch, Lock, Puzzle, Bell, Key } from "@lucide/svelte";
|
|
6
6
|
import ProfilePage from "./ProfilePage.svelte";
|
|
7
7
|
import AppearanceSettings from "./AppearanceSettings.svelte";
|
|
8
8
|
import AboutSettings from "./AboutSettings.svelte";
|
|
@@ -60,19 +60,25 @@
|
|
|
60
60
|
{
|
|
61
61
|
group: "settings.general",
|
|
62
62
|
items: [
|
|
63
|
-
{ key: "
|
|
63
|
+
{ key: "profile", path: "/settings/profile", icon: User, label: "settings.profile" },
|
|
64
|
+
{ key: "appearance", path: "/settings/appearance", icon: Palette, label: "settings.appearance" },
|
|
65
|
+
{ key: "notifications", path: "/settings/notifications", icon: Bell, label: "settings.notifications" },
|
|
64
66
|
],
|
|
65
67
|
},
|
|
66
68
|
{
|
|
67
69
|
group: "settings.workspace",
|
|
68
70
|
items: [
|
|
69
|
-
{ key: "
|
|
70
|
-
|
|
71
|
+
{ key: "security", path: "/settings/security", icon: Lock, label: "settings.security" },
|
|
72
|
+
...(authProvider ? [
|
|
73
|
+
{ key: "roles", path: "/settings/roles", icon: Shield, label: "settings.rolesAndPermissions" },
|
|
74
|
+
] : []),
|
|
75
|
+
{ key: "integrations", path: "/settings/integrations", icon: Puzzle, label: "settings.integrations" },
|
|
71
76
|
],
|
|
72
77
|
},
|
|
73
78
|
{
|
|
74
79
|
group: "settings.developer",
|
|
75
80
|
items: [
|
|
81
|
+
{ key: "api", path: "/settings/api", icon: Key, label: "settings.api" },
|
|
76
82
|
...(authProvider ? [
|
|
77
83
|
{ key: "audit", path: "/settings/audit", icon: FileSearch, label: "settings.auditLogs" },
|
|
78
84
|
] : []),
|
|
@@ -86,59 +92,88 @@
|
|
|
86
92
|
|
|
87
93
|
import { getParams, getRoute } from "../router-state.svelte.js";
|
|
88
94
|
|
|
89
|
-
function normalizeKey(tab: string | undefined): string {
|
|
90
|
-
if (!tab) return "account";
|
|
91
|
-
if (["profile", "appearance", "notifications"].includes(tab)) return "account";
|
|
92
|
-
if (["security", "roles"].includes(tab)) return "access";
|
|
93
|
-
if (["integrations", "api"].includes(tab)) return "developer";
|
|
94
|
-
return tab;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function pathForKey(key: string): string {
|
|
98
|
-
return `/settings/${key}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
95
|
let activeKey = $derived.by(() => {
|
|
102
96
|
const tab = getParams().tab;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
if (!tab) return "profile";
|
|
98
|
+
const alias = tab === "api-keys" ? "api" : tab;
|
|
99
|
+
if (sectionKeys.has(alias)) return alias;
|
|
100
|
+
return content ? alias : "profile";
|
|
106
101
|
});
|
|
107
102
|
|
|
103
|
+
// Default redirect to profile
|
|
108
104
|
$effect(() => {
|
|
109
105
|
if (getRoute() === "/settings") {
|
|
110
|
-
navigate("/settings/
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const tab = getParams().tab;
|
|
115
|
-
const normalized = normalizeKey(tab);
|
|
116
|
-
if (tab && tab !== normalized && sectionKeys.has(normalized)) {
|
|
117
|
-
navigate(pathForKey(normalized));
|
|
106
|
+
navigate("/settings/profile");
|
|
118
107
|
}
|
|
119
108
|
});
|
|
120
109
|
</script>
|
|
121
110
|
|
|
122
|
-
<div class="min-h-full
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
<div class="flex flex-col lg:flex-row gap-0 min-h-full">
|
|
112
|
+
<!-- Left sidebar navigation -->
|
|
113
|
+
<nav class="w-full lg:w-56 shrink-0 bg-muted/10 shadow-[inset_0_-1px_0_rgba(0,0,0,0.05)] lg:shadow-[inset_-1px_0_0_rgba(0,0,0,0.05)] dark:shadow-[inset_-1px_0_0_rgba(255,255,255,0.05)]">
|
|
114
|
+
<!-- Mobile: horizontal scroll tabs -->
|
|
115
|
+
<div class="flex lg:hidden overflow-x-auto px-4 py-2 gap-1">
|
|
116
|
+
{#each resolvedSections as section, _i (_i)}
|
|
117
|
+
{#each section.items as item, _j (_j)}
|
|
118
|
+
{@const active = activeKey === item.key}
|
|
119
|
+
<button
|
|
120
|
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md whitespace-nowrap transition-colors
|
|
121
|
+
{active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-background/50'}"
|
|
122
|
+
onclick={() => navigate(item.path)}
|
|
123
|
+
>
|
|
124
|
+
<item.icon class="h-4 w-4" />
|
|
125
|
+
{t(item.label)}
|
|
126
|
+
</button>
|
|
127
|
+
{/each}
|
|
128
|
+
{/each}
|
|
127
129
|
</div>
|
|
128
|
-
</div>
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
<!-- Desktop: vertical nav with groups -->
|
|
132
|
+
<div class="hidden lg:block py-6 px-3 space-y-6">
|
|
133
|
+
<div class="px-3">
|
|
134
|
+
<h2 class="text-lg font-semibold text-foreground">{t("settings.title")}</h2>
|
|
135
|
+
<p class="text-xs text-muted-foreground mt-1">{t("settings.settingsDescription")}</p>
|
|
136
|
+
</div>
|
|
137
|
+
{#each resolvedSections as section, _i (_i)}
|
|
138
|
+
<div>
|
|
139
|
+
<h3 class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
140
|
+
{t(section.group)}
|
|
141
|
+
</h3>
|
|
142
|
+
<div class="space-y-0.5">
|
|
143
|
+
{#each section.items as item, _j (_j)}
|
|
144
|
+
{@const active = activeKey === item.key}
|
|
145
|
+
<button
|
|
146
|
+
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all
|
|
147
|
+
{active
|
|
148
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
149
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
150
|
+
onclick={() => navigate(item.path)}
|
|
151
|
+
>
|
|
152
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
153
|
+
{t(item.label)}
|
|
154
|
+
</button>
|
|
155
|
+
{/each}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{/each}
|
|
159
|
+
</div>
|
|
160
|
+
</nav>
|
|
161
|
+
|
|
162
|
+
<!-- Right content area -->
|
|
163
|
+
<div class="flex-1 p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
|
|
164
|
+
{#if activeKey === "profile"}
|
|
132
165
|
{#if profile}{@render profile()}{:else}<ProfilePage />{/if}
|
|
166
|
+
{:else if activeKey === "appearance"}
|
|
133
167
|
{#if appearance}{@render appearance()}{:else}<AppearanceSettings />{/if}
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
{:else if activeKey === "roles"}
|
|
169
|
+
{#if roles}{@render roles()}{:else}<RolesSettings />{/if}
|
|
170
|
+
{:else if activeKey === "security"}
|
|
136
171
|
{#if security}{@render security()}{:else}<SecuritySettings />{/if}
|
|
137
|
-
|
|
138
|
-
{#if roles}{@render roles()}{:else}<RolesSettings />{/if}
|
|
139
|
-
{/if}
|
|
140
|
-
{:else if activeKey === "developer"}
|
|
172
|
+
{:else if activeKey === "integrations"}
|
|
141
173
|
{#if integrations}{@render integrations()}{:else}<IntegrationsSettings />{/if}
|
|
174
|
+
{:else if activeKey === "notifications"}
|
|
175
|
+
{#if notifications}{@render notifications()}{:else}<NotificationsSettings />{/if}
|
|
176
|
+
{:else if activeKey === "api"}
|
|
142
177
|
{#if api}{@render api()}{:else}<ApiSettings />{/if}
|
|
143
178
|
{:else if activeKey === "audit"}
|
|
144
179
|
{#if audit}{@render audit()}{:else}<AuditLogViewer />{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// ShowInferencer — 基于资源定义零配置渲染详情页
|
|
3
|
+
// 从 URL 自动提取 :id 参数
|
|
4
|
+
import ShowPage from './ShowPage.svelte';
|
|
5
|
+
import { useParsed } from '@svadmin/core';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
resourceName: string;
|
|
9
|
+
/** 可选:显式指定 id,默认从 URL 解析 */
|
|
10
|
+
id?: string | number;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { resourceName, id, class: className = '' }: Props = $props();
|
|
15
|
+
const parsed = useParsed();
|
|
16
|
+
const resolvedId = $derived(id ?? parsed.id ?? '');
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#if resolvedId}
|
|
20
|
+
<ShowPage {resourceName} id={resolvedId} class={className} />
|
|
21
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
resourceName: string;
|
|
3
|
+
/** 可选:显式指定 id,默认从 URL 解析 */
|
|
4
|
+
id?: string | number;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const ShowInferencer: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type ShowInferencer = ReturnType<typeof ShowInferencer>;
|
|
9
|
+
export default ShowInferencer;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import CloneButton from './buttons/CloneButton.svelte';
|
|
13
13
|
import RefreshButton from './buttons/RefreshButton.svelte';
|
|
14
14
|
|
|
15
|
-
let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
|
|
15
|
+
let { resourceName, id, class: className = '' } = $props<{ resourceName: string; id: string | number; class?: string }>();
|
|
16
16
|
|
|
17
17
|
const resource = $derived(getResource(resourceName));
|
|
18
18
|
const showFields = $derived(resource.fields.filter(f => f.showInShow !== false));
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const query = useShow({ get resource() { return resourceName; }, get id() { return id; } });
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
|
-
<div class="space-y-6">
|
|
23
|
+
<div class="space-y-6 {className}">
|
|
24
24
|
<PageHeader title="{resource.label} {t('common.detail')}">
|
|
25
25
|
{#snippet actions()}
|
|
26
26
|
<ListButton resource={resourceName} hideText />
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
</PageHeader>
|
|
39
39
|
|
|
40
40
|
{#if query.isLoading}
|
|
41
|
-
<Card.Root class="overflow-hidden border-border/
|
|
42
|
-
<Card.Content class="
|
|
41
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
42
|
+
<Card.Content class="p-0">
|
|
43
43
|
{#each showFields.slice(0, 6) as _, i (i)}
|
|
44
|
-
<div class="flex flex-col
|
|
44
|
+
<div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
45
45
|
<Skeleton class="h-4 w-1/2 sm:w-1/4" />
|
|
46
46
|
<Skeleton class="h-4 w-3/4 sm:w-2/5 mt-1 sm:mt-0 sm:ml-auto" />
|
|
47
47
|
</div>
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
</Card.Content>
|
|
50
50
|
</Card.Root>
|
|
51
51
|
{:else if query.data?.data}
|
|
52
|
-
<Card.Root class="overflow-hidden border-border/
|
|
53
|
-
<Card.Content class="
|
|
52
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
53
|
+
<Card.Content class="p-0">
|
|
54
54
|
{#each showFields as field, i (i)}
|
|
55
55
|
{@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
|
|
56
56
|
{@const DisplayComponent = getDisplayComponent(field.type)}
|
|
57
|
-
<div class="flex flex-col
|
|
58
|
-
<div class="
|
|
59
|
-
<div class="text-sm text-foreground
|
|
57
|
+
<div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
58
|
+
<div class="sm:w-1/3 text-xs sm:text-sm font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
|
|
59
|
+
<div class="sm:w-2/3 text-sm text-foreground">
|
|
60
60
|
{#if DisplayComponent && value != null}
|
|
61
61
|
<DisplayComponent
|
|
62
62
|
{value}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
</Card.Content>
|
|
73
73
|
</Card.Root>
|
|
74
74
|
{:else}
|
|
75
|
-
<Card.Root class="overflow-hidden border-border/
|
|
75
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
76
76
|
<Card.Content class="p-8 text-center">
|
|
77
77
|
<p class="text-muted-foreground">{t('common.noData')}</p>
|
|
78
78
|
{#if query.isError}
|