@svadmin/ui 0.35.0 → 0.36.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.
@@ -915,7 +915,7 @@
915
915
  <div class="flex items-center gap-2">
916
916
  <span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
917
917
  <select
918
- class="h-8 w-[70px] px-1 py-1 flex items-center justify-between rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
918
+ class="h-8 w-[70px] px-2 rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
919
919
  value={String(pagination.pageSize ?? 10)}
920
920
  onchange={(e) => {
921
921
  const size = Number((e.target as HTMLSelectElement).value);
@@ -930,7 +930,7 @@
930
930
  <option value="100">100</option>
931
931
  </select>
932
932
  </div>
933
- <PaginationUI.Root>
933
+ <PaginationUI.Root class="sm:w-auto sm:justify-end sm:mx-0">
934
934
  <PaginationUI.Content>
935
935
  <PaginationUI.Item>
936
936
  <PaginationUI.Previous
@@ -26,7 +26,7 @@
26
26
  );
27
27
  </script>
28
28
 
29
- <div class="flex min-h-[70vh] items-center justify-center p-4">
29
+ <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="error">
30
30
  <Card.Card class="w-full max-w-md border-border/60">
31
31
  <Card.CardContent class="space-y-6 px-6 py-10 text-center">
32
32
  <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-lg bg-muted/50 text-muted-foreground">
@@ -139,7 +139,7 @@
139
139
  <div
140
140
  class="flex-1 flex flex-col overflow-hidden transition-all duration-300"
141
141
  class:md:ml-72={!collapsed}
142
- class:md:ml-20={collapsed}
142
+ class:md:ml-16={collapsed}
143
143
  >
144
144
  <!-- Header with mobile hamburger -->
145
145
  <Header
@@ -19,13 +19,13 @@
19
19
  }: Props = $props();
20
20
  </script>
21
21
 
22
- <div class="rounded-3xl border border-border/70 bg-card/90 p-5 shadow-sm shadow-slate-900/[0.03] backdrop-blur {className}">
22
+ <div class="border-b border-border/70 pb-5 {className}" data-svadmin-page-header>
23
23
  {#if showBreadcrumbs}
24
24
  <div class="mb-4">
25
25
  <Breadcrumbs />
26
26
  </div>
27
27
  {/if}
28
- <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
28
+ <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
29
29
  <div class="min-w-0">
30
30
  <h1 class="truncate text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
31
31
  {#if description}
@@ -33,7 +33,7 @@
33
33
  {/if}
34
34
  </div>
35
35
  {#if actions}
36
- <div class="flex flex-wrap items-center gap-2">
36
+ <div class="flex w-full flex-wrap items-center gap-2 lg:w-auto lg:justify-end">
37
37
  {@render actions()}
38
38
  </div>
39
39
  {/if}
@@ -59,13 +59,12 @@
59
59
  </script>
60
60
 
61
61
  <div class="space-y-6" data-svadmin-resource-operations={resourceName}>
62
- <section class="grid gap-4 xl:grid-cols-[0.82fr_1.18fr]">
63
- <Card.Root class="rounded-[2rem] border-border/70 bg-card">
64
- <Card.Content class="p-6 sm:p-8">
65
- <Badge class="mb-4 rounded-full bg-primary/10 text-primary hover:bg-primary/10">{eyebrow}</Badge>
62
+ <section class="grid gap-4 border-b border-border/60 pb-5 xl:grid-cols-[0.82fr_1.18fr]" data-svadmin-ops-hero>
63
+ <div class="min-w-0 py-1">
64
+ <Badge>{eyebrow}</Badge>
66
65
  <div class="flex items-start gap-4">
67
66
  {#if Icon}
68
- <span class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm shadow-primary/25">
67
+ <span class="flex h-10 w-10 shrink-0 items-center justify-center text-primary">
69
68
  <Icon class="h-6 w-6" />
70
69
  </span>
71
70
  {/if}
@@ -74,15 +73,14 @@
74
73
  <p class="mt-2 max-w-2xl text-sm text-muted-foreground">{description}</p>
75
74
  </div>
76
75
  </div>
77
- <Button class="mt-6 rounded-xl" onclick={() => navigate(`/${resourceName}/create`)}>
76
+ <Button class="mt-6" onclick={() => navigate(`/${resourceName}/create`)}>
78
77
  {actionLabel}
79
78
  </Button>
80
- </Card.Content>
81
- </Card.Root>
79
+ </div>
82
80
 
83
81
  <div class="grid gap-4 md:grid-cols-3">
84
82
  {#each metrics as metric (metric.label)}
85
- <Card.Root class="rounded-[2rem] border-border/70 bg-card">
83
+ <Card.Root data-svadmin-metric-card>
86
84
  <Card.Content class="p-5">
87
85
  <p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">{metric.label}</p>
88
86
  <p class="mt-2 text-3xl font-semibold">{metric.value}</p>
@@ -97,7 +95,7 @@
97
95
 
98
96
  <div class="grid gap-4 xl:grid-cols-[0.76fr_1.24fr]">
99
97
  <div class="space-y-4">
100
- <Card.Root class="rounded-[2rem] border-border/70 bg-card">
98
+ <Card.Root data-svadmin-panel-card>
101
99
  <Card.Header>
102
100
  <Card.Title>{tableLabel}</Card.Title>
103
101
  {#if tableDescription}
@@ -106,7 +104,7 @@
106
104
  </Card.Header>
107
105
  <Card.Content class="space-y-3">
108
106
  {#each lanes as lane (lane.label)}
109
- <div class="rounded-2xl border border-border/60 bg-muted/20 p-4">
107
+ <div class="py-3">
110
108
  <div class="flex items-center justify-between gap-3">
111
109
  <p class="font-medium capitalize">{lane.label}</p>
112
110
  <Badge variant="outline">{lane.value}</Badge>
@@ -116,19 +114,19 @@
116
114
  {/if}
117
115
  </div>
118
116
  {:else}
119
- <p class="rounded-2xl border border-border/60 bg-muted/20 p-4 text-sm text-muted-foreground">{emptyLanesText}</p>
117
+ <p class="py-3 text-sm text-muted-foreground">{emptyLanesText}</p>
120
118
  {/each}
121
119
  </Card.Content>
122
120
  </Card.Root>
123
121
 
124
122
  {#if highlights.length > 0}
125
- <Card.Root class="rounded-[2rem] border-primary/20 bg-primary/5">
123
+ <Card.Root data-svadmin-panel-card>
126
124
  <Card.Header>
127
125
  <Card.Title>{highlightsLabel}</Card.Title>
128
126
  </Card.Header>
129
127
  <Card.Content class="space-y-3">
130
128
  {#each highlights as item (item.title)}
131
- <div class="rounded-2xl border border-primary/15 bg-background/80 p-4">
129
+ <div class="py-3">
132
130
  <div class="flex items-center justify-between gap-3">
133
131
  <p class="font-medium">{item.title}</p>
134
132
  {#if item.badge}
@@ -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, Palette, Info, Shield, FileSearch, Lock, Puzzle, Bell, Key } from "@lucide/svelte";
5
+ import { User, Info, FileSearch, Lock, Puzzle } 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,25 +60,19 @@
60
60
  {
61
61
  group: "settings.general",
62
62
  items: [
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" },
63
+ { key: "account", path: "/settings/account", icon: User, label: "settings.accountPreferences" },
66
64
  ],
67
65
  },
68
66
  {
69
67
  group: "settings.workspace",
70
68
  items: [
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" },
69
+ { key: "access", path: "/settings/access", icon: Lock, label: "settings.accessSecurity" },
70
+ { key: "developer", path: "/settings/developer", icon: Puzzle, label: "settings.integrationApi" },
76
71
  ],
77
72
  },
78
73
  {
79
74
  group: "settings.developer",
80
75
  items: [
81
- { key: "api", path: "/settings/api", icon: Key, label: "settings.api" },
82
76
  ...(authProvider ? [
83
77
  { key: "audit", path: "/settings/audit", icon: FileSearch, label: "settings.auditLogs" },
84
78
  ] : []),
@@ -92,87 +86,59 @@
92
86
 
93
87
  import { getParams, getRoute } from "../router-state.svelte.js";
94
88
 
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
+
95
101
  let activeKey = $derived.by(() => {
96
102
  const tab = getParams().tab;
97
- if (!tab) return "profile";
98
- if (sectionKeys.has(tab)) return tab;
99
- return content ? tab : "profile";
103
+ const normalized = normalizeKey(tab);
104
+ if (sectionKeys.has(normalized)) return normalized;
105
+ return content ? normalized : "account";
100
106
  });
101
107
 
102
- // Default redirect to profile
103
108
  $effect(() => {
104
109
  if (getRoute() === "/settings") {
105
- navigate("/settings/profile");
110
+ navigate("/settings/account");
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
118
  }
107
119
  });
108
120
  </script>
109
121
 
110
- <div class="flex min-h-full flex-col overflow-hidden rounded-3xl border border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03] backdrop-blur lg:flex-row">
111
- <!-- Left sidebar navigation -->
112
- <nav class="w-full shrink-0 border-b border-border/70 bg-muted/20 lg:w-64 lg:border-b-0 lg:border-r">
113
- <!-- Mobile: horizontal scroll tabs -->
114
- <div class="flex gap-2 overflow-x-auto px-4 py-3 lg:hidden">
115
- {#each resolvedSections as section, _i (_i)}
116
- {#each section.items as item, _j (_j)}
117
- {@const active = activeKey === item.key}
118
- <button
119
- class="flex items-center gap-2 whitespace-nowrap rounded-xl px-3 py-2 text-sm font-medium transition-colors
120
- {active ? 'bg-primary/10 text-primary ring-1 ring-primary/15' : 'text-muted-foreground hover:bg-background/70 hover:text-foreground'}"
121
- onclick={() => navigate(item.path)}
122
- >
123
- <item.icon class="h-4 w-4" />
124
- {t(item.label)}
125
- </button>
126
- {/each}
127
- {/each}
128
- </div>
129
-
130
- <!-- Desktop: vertical nav with groups -->
131
- <div class="hidden space-y-6 px-4 py-6 lg:block">
132
- <div class="rounded-2xl border border-border/60 bg-background/70 p-4">
133
- <h2 class="text-lg font-semibold tracking-tight text-foreground">{t("settings.title")}</h2>
134
- <p class="text-xs text-muted-foreground mt-1">{t("settings.settingsDescription")}</p>
135
- </div>
136
- {#each resolvedSections as section, _i (_i)}
137
- <div>
138
- <h3 class="mb-2 px-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/60">
139
- {t(section.group)}
140
- </h3>
141
- <div class="space-y-1">
142
- {#each section.items as item, _j (_j)}
143
- {@const active = activeKey === item.key}
144
- <button
145
- class="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-all
146
- {active
147
- ? 'bg-primary/10 text-primary font-medium ring-1 ring-primary/15 shadow-sm shadow-primary/10'
148
- : 'text-muted-foreground hover:bg-background/75 hover:text-foreground'}"
149
- onclick={() => navigate(item.path)}
150
- >
151
- <item.icon class="h-4 w-4 shrink-0" />
152
- {t(item.label)}
153
- </button>
154
- {/each}
155
- </div>
156
- </div>
157
- {/each}
122
+ <div class="min-h-full max-w-6xl">
123
+ <div class="mb-6 border-b border-border/70 pb-4">
124
+ <div class="min-w-0">
125
+ <h2 class="text-2xl font-semibold tracking-tight text-foreground">{t("settings.title")}</h2>
126
+ <p class="mt-1 text-sm text-muted-foreground">{t("settings.settingsDescription")}</p>
158
127
  </div>
159
- </nav>
128
+ </div>
160
129
 
161
- <!-- Right content area -->
162
- <div class="flex-1 bg-background/60 p-5 sm:p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
163
- {#if activeKey === "profile"}
130
+ <div class="{activeKey === 'access' || activeKey === 'audit' || activeKey === 'developer' ? 'max-w-6xl' : 'max-w-3xl'} space-y-6">
131
+ {#if activeKey === "account"}
164
132
  {#if profile}{@render profile()}{:else}<ProfilePage />{/if}
165
- {:else if activeKey === "appearance"}
166
133
  {#if appearance}{@render appearance()}{:else}<AppearanceSettings />{/if}
167
- {:else if activeKey === "roles"}
168
- {#if roles}{@render roles()}{:else}<RolesSettings />{/if}
169
- {:else if activeKey === "security"}
134
+ {#if notifications}{@render notifications()}{:else}<NotificationsSettings />{/if}
135
+ {:else if activeKey === "access"}
170
136
  {#if security}{@render security()}{:else}<SecuritySettings />{/if}
171
- {:else if activeKey === "integrations"}
137
+ {#if authProvider}
138
+ {#if roles}{@render roles()}{:else}<RolesSettings />{/if}
139
+ {/if}
140
+ {:else if activeKey === "developer"}
172
141
  {#if integrations}{@render integrations()}{:else}<IntegrationsSettings />{/if}
173
- {:else if activeKey === "notifications"}
174
- {#if notifications}{@render notifications()}{:else}<NotificationsSettings />{/if}
175
- {:else if activeKey === "api"}
176
142
  {#if api}{@render api()}{:else}<ApiSettings />{/if}
177
143
  {:else if activeKey === "audit"}
178
144
  {#if audit}{@render audit()}{:else}<AuditLogViewer />{/if}
@@ -38,10 +38,10 @@
38
38
  </PageHeader>
39
39
 
40
40
  {#if query.isLoading}
41
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
41
+ <Card.Root class="overflow-hidden border-border/60 bg-card shadow-none" data-svadmin-show-card>
42
42
  <Card.Content class="space-y-3 p-4 sm:p-5">
43
43
  {#each showFields.slice(0, 6) as _, i (i)}
44
- <div class="flex flex-col rounded-2xl border border-border/50 bg-muted/20 px-4 py-3 sm:flex-row sm:py-4">
44
+ <div class="flex flex-col border-b border-border/50 px-1 py-4 sm:flex-row sm:py-4">
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,12 +49,12 @@
49
49
  </Card.Content>
50
50
  </Card.Root>
51
51
  {:else if query.data?.data}
52
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
52
+ <Card.Root class="overflow-hidden border-border/60 bg-card shadow-none" data-svadmin-show-card>
53
53
  <Card.Content class="space-y-3 p-4 sm:p-5">
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 rounded-2xl border border-border/55 bg-background/70 px-4 py-3 transition-colors hover:bg-muted/20 sm:flex-row sm:items-start sm:py-4 {i % 2 === 1 ? 'bg-muted/15' : ''}">
57
+ <div class="flex flex-col border-b border-border/50 px-1 py-4 transition-colors hover:bg-muted/20 sm:flex-row sm:items-start sm:py-4 {i % 2 === 1 ? 'bg-muted/10' : ''}">
58
58
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground sm:mb-0 sm:w-1/3">{field.label}</div>
59
59
  <div class="text-sm text-foreground sm:w-2/3">
60
60
  {#if DisplayComponent && value != null}
@@ -72,7 +72,7 @@
72
72
  </Card.Content>
73
73
  </Card.Root>
74
74
  {:else}
75
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
75
+ <Card.Root class="overflow-hidden border-border/60 bg-card shadow-none" data-svadmin-show-card>
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}