@svadmin/ui 0.4.1 → 0.5.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.2",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -40,7 +40,8 @@
40
40
  "author": "zuohuadong",
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "git+https://github.com/zuohuadong/svadmin.git"
43
+ "url": "https://github.com/zuohuadong/svadmin.git",
44
+ "directory": "packages/ui"
44
45
  },
45
46
  "homepage": "https://github.com/zuohuadong/svadmin#readme",
46
47
  "bugs": {
package/src/app.css CHANGED
@@ -1,65 +1,94 @@
1
- /* @svadmin/ui — Design tokens (CSS custom properties)
2
- * Import this in your app's CSS AFTER @import "tailwindcss"
3
- * or import it standalone for the color variables.
1
+ /* @svadmin/ui — Design Tokens (Shadcn New York style)
2
+ * Auto-imported by AdminApp. Can be overridden by consumer's CSS.
3
+ *
4
+ * Based on shadcn/ui "New York" theme with oklch() color space.
5
+ * Differences from "Default" style:
6
+ * - Smaller border-radius (0.5rem vs 0.625rem)
7
+ * - Refined color palette with deeper contrast
8
+ * - Chart color tokens included
9
+ * - More polished dark mode
4
10
  */
5
11
 
6
- :root {
7
- --radius: 0.625rem;
8
- --background: oklch(1 0 0);
9
- --foreground: oklch(0.145 0 0);
10
- --card: oklch(1 0 0);
11
- --card-foreground: oklch(0.145 0 0);
12
- --popover: oklch(1 0 0);
13
- --popover-foreground: oklch(0.145 0 0);
14
- --primary: oklch(0.205 0 0);
15
- --primary-foreground: oklch(0.985 0 0);
16
- --secondary: oklch(0.97 0 0);
17
- --secondary-foreground: oklch(0.205 0 0);
18
- --muted: oklch(0.97 0 0);
19
- --muted-foreground: oklch(0.556 0 0);
20
- --accent: oklch(0.97 0 0);
21
- --accent-foreground: oklch(0.205 0 0);
22
- --destructive: oklch(0.577 0.245 27.325);
23
- --border: oklch(0.922 0 0);
24
- --input: oklch(0.922 0 0);
25
- --ring: oklch(0.708 0 0);
26
- --sidebar: oklch(0.985 0 0);
27
- --sidebar-foreground: oklch(0.145 0 0);
28
- --sidebar-primary: oklch(0.205 0 0);
29
- --sidebar-primary-foreground: oklch(0.985 0 0);
30
- --sidebar-accent: oklch(0.97 0 0);
31
- --sidebar-accent-foreground: oklch(0.205 0 0);
32
- --sidebar-border: oklch(0.922 0 0);
33
- --sidebar-ring: oklch(0.708 0 0);
12
+ @layer base {
13
+ :root {
14
+ --radius: 0.5rem;
15
+ --background: oklch(1 0 0);
16
+ --foreground: oklch(0.145 0 0);
17
+ --card: oklch(1 0 0);
18
+ --card-foreground: oklch(0.145 0 0);
19
+ --popover: oklch(1 0 0);
20
+ --popover-foreground: oklch(0.145 0 0);
21
+ --primary: oklch(0.205 0 0);
22
+ --primary-foreground: oklch(0.985 0 0);
23
+ --secondary: oklch(0.965 0 0);
24
+ --secondary-foreground: oklch(0.205 0 0);
25
+ --muted: oklch(0.965 0 0);
26
+ --muted-foreground: oklch(0.45 0 0);
27
+ --accent: oklch(0.965 0 0);
28
+ --accent-foreground: oklch(0.205 0 0);
29
+ --destructive: oklch(0.577 0.245 27.325);
30
+ --destructive-foreground: oklch(0.985 0 0);
31
+ --border: oklch(0.922 0 0);
32
+ --input: oklch(0.922 0 0);
33
+ --ring: oklch(0.708 0 0);
34
+ --chart-1: oklch(0.646 0.222 41.116);
35
+ --chart-2: oklch(0.6 0.118 184.704);
36
+ --chart-3: oklch(0.398 0.07 227.392);
37
+ --chart-4: oklch(0.828 0.189 84.429);
38
+ --chart-5: oklch(0.769 0.188 70.08);
39
+ --sidebar: oklch(0.985 0 0);
40
+ --sidebar-foreground: oklch(0.145 0 0);
41
+ --sidebar-primary: oklch(0.205 0 0);
42
+ --sidebar-primary-foreground: oklch(0.985 0 0);
43
+ --sidebar-accent: oklch(0.965 0 0);
44
+ --sidebar-accent-foreground: oklch(0.205 0 0);
45
+ --sidebar-border: oklch(0.922 0 0);
46
+ --sidebar-ring: oklch(0.708 0 0);
47
+ }
48
+
49
+ .dark {
50
+ --background: oklch(0.145 0 0);
51
+ --foreground: oklch(0.985 0 0);
52
+ --card: oklch(0.178 0 0);
53
+ --card-foreground: oklch(0.985 0 0);
54
+ --popover: oklch(0.178 0 0);
55
+ --popover-foreground: oklch(0.985 0 0);
56
+ --primary: oklch(0.985 0 0);
57
+ --primary-foreground: oklch(0.205 0 0);
58
+ --secondary: oklch(0.269 0 0);
59
+ --secondary-foreground: oklch(0.985 0 0);
60
+ --muted: oklch(0.269 0 0);
61
+ --muted-foreground: oklch(0.708 0 0);
62
+ --accent: oklch(0.269 0 0);
63
+ --accent-foreground: oklch(0.985 0 0);
64
+ --destructive: oklch(0.704 0.191 22.216);
65
+ --destructive-foreground: oklch(0.985 0 0);
66
+ --border: oklch(1 0 0 / 10%);
67
+ --input: oklch(1 0 0 / 15%);
68
+ --ring: oklch(0.556 0 0);
69
+ --chart-1: oklch(0.488 0.243 264.376);
70
+ --chart-2: oklch(0.696 0.17 162.48);
71
+ --chart-3: oklch(0.769 0.188 70.08);
72
+ --chart-4: oklch(0.627 0.265 303.9);
73
+ --chart-5: oklch(0.645 0.246 16.439);
74
+ --sidebar: oklch(0.178 0 0);
75
+ --sidebar-foreground: oklch(0.985 0 0);
76
+ --sidebar-primary: oklch(0.488 0.243 264.376);
77
+ --sidebar-primary-foreground: oklch(0.985 0 0);
78
+ --sidebar-accent: oklch(0.269 0 0);
79
+ --sidebar-accent-foreground: oklch(0.985 0 0);
80
+ --sidebar-border: oklch(1 0 0 / 10%);
81
+ --sidebar-ring: oklch(0.556 0 0);
82
+ }
34
83
  }
35
84
 
36
- .dark {
37
- --background: oklch(0.145 0 0);
38
- --foreground: oklch(0.985 0 0);
39
- --card: oklch(0.205 0 0);
40
- --card-foreground: oklch(0.985 0 0);
41
- --popover: oklch(0.205 0 0);
42
- --popover-foreground: oklch(0.985 0 0);
43
- --primary: oklch(0.922 0 0);
44
- --primary-foreground: oklch(0.205 0 0);
45
- --secondary: oklch(0.269 0 0);
46
- --secondary-foreground: oklch(0.985 0 0);
47
- --muted: oklch(0.269 0 0);
48
- --muted-foreground: oklch(0.708 0 0);
49
- --accent: oklch(0.269 0 0);
50
- --accent-foreground: oklch(0.985 0 0);
51
- --destructive: oklch(0.704 0.191 22.216);
52
- --border: oklch(1 0 0 / 10%);
53
- --input: oklch(1 0 0 / 15%);
54
- --ring: oklch(0.556 0 0);
55
- --sidebar: oklch(0.205 0 0);
56
- --sidebar-foreground: oklch(0.985 0 0);
57
- --sidebar-primary: oklch(0.488 0.243 264.376);
58
- --sidebar-primary-foreground: oklch(0.985 0 0);
59
- --sidebar-accent: oklch(0.269 0 0);
60
- --sidebar-accent-foreground: oklch(0.985 0 0);
61
- --sidebar-border: oklch(1 0 0 / 10%);
62
- --sidebar-ring: oklch(0.556 0 0);
85
+ /* Typography — Inter font via Bunny Fonts (global CDN, works in China) */
86
+ @import url('https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap');
87
+
88
+ body {
89
+ font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
90
+ -webkit-font-smoothing: antialiased;
91
+ -moz-osx-font-smoothing: grayscale;
63
92
  }
64
93
 
65
94
  /* Scrollbar styling */
@@ -67,3 +96,5 @@
67
96
  ::-webkit-scrollbar-track { background: transparent; }
68
97
  ::-webkit-scrollbar-thumb { background: oklch(0.8 0 0); border-radius: 3px; }
69
98
  ::-webkit-scrollbar-thumb:hover { background: oklch(0.65 0 0); }
99
+ .dark ::-webkit-scrollbar-thumb { background: oklch(0.35 0 0); }
100
+ .dark ::-webkit-scrollbar-thumb:hover { background: oklch(0.45 0 0); }
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ // Auto-inject design tokens so consumers don't need to manually import
3
+ import '../app.css';
2
4
  import type { Snippet } from 'svelte';
3
5
  import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider } from '@svadmin/core';
4
6
  import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider } from '@svadmin/core';
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { canAccess } from '@svadmin/core/permissions';
4
- import type { Action } from '@svadmin/core/permissions';
3
+ import { useCan } from '@svadmin/core';
4
+ import type { Action } from '@svadmin/core';
5
5
 
6
6
  let { resource, action, params, children, fallback } = $props<{
7
7
  resource: string;
@@ -11,10 +11,12 @@
11
11
  fallback?: Snippet;
12
12
  }>();
13
13
 
14
- const result = $derived(canAccess(resource, action, params));
14
+ const can = $derived(useCan(resource, action, params));
15
15
  </script>
16
16
 
17
- {#if result.can}
17
+ {#if can.isLoading}
18
+ <!-- Loading: nothing rendered -->
19
+ {:else if can.allowed}
18
20
  {@render children()}
19
21
  {:else if fallback}
20
22
  {@render fallback()}
@@ -8,11 +8,34 @@
8
8
  import * as Card from './ui/card/index.js';
9
9
  import * as Alert from './ui/alert/index.js';
10
10
  import PasswordInput from './PasswordInput.svelte';
11
+ import { Separator } from './ui/separator/index.js';
11
12
  import { LogIn, Mail, Loader2, AlertCircle } from 'lucide-svelte';
12
13
 
13
- let { title = 'Admin', onSuccess } = $props<{
14
+ /**
15
+ * Social provider config for the login page.
16
+ * Each entry renders a button that calls `onClick()` when clicked.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const socialProviders = [
21
+ * { name: 'Google', icon: '🔵', onClick: () => googleAuth.login({}) },
22
+ * { name: 'GitHub', icon: '🐙', onClick: () => githubAuth.login({}) },
23
+ * ];
24
+ * ```
25
+ */
26
+ interface SocialProvider {
27
+ /** Display name (e.g., 'Google') */
28
+ name: string;
29
+ /** Icon character, emoji, or SVG string */
30
+ icon?: string;
31
+ /** Handler to initiate social login */
32
+ onClick: () => void | Promise<void>;
33
+ }
34
+
35
+ let { title = 'Admin', onSuccess, socialProviders = [] } = $props<{
14
36
  title?: string;
15
37
  onSuccess?: () => void;
38
+ socialProviders?: SocialProvider[];
16
39
  }>();
17
40
 
18
41
  const login = useLogin();
@@ -97,6 +120,30 @@
97
120
  </Button>
98
121
  </form>
99
122
 
123
+ {#if socialProviders.length > 0}
124
+ <div class="relative my-5">
125
+ <Separator />
126
+ <span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-xs text-muted-foreground">
127
+ {t('auth.orContinueWith')}
128
+ </span>
129
+ </div>
130
+
131
+ <div class="grid gap-2" class:grid-cols-2={socialProviders.length >= 2}>
132
+ {#each socialProviders as provider (provider.name)}
133
+ <Button
134
+ variant="outline"
135
+ class="w-full"
136
+ onclick={provider.onClick}
137
+ >
138
+ {#if provider.icon}
139
+ <span class="mr-2">{provider.icon}</span>
140
+ {/if}
141
+ {provider.name}
142
+ </Button>
143
+ {/each}
144
+ </div>
145
+ {/if}
146
+
100
147
  {#if authProvider.register}
101
148
  <div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
102
149
  <span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import { useGetIdentity, useUpdatePassword } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import { Button } from './ui/button/index.js';
5
+ import { Label } from './ui/label/index.js';
6
+ import * as Card from './ui/card/index.js';
7
+ import * as Alert from './ui/alert/index.js';
8
+ import PasswordInput from './PasswordInput.svelte';
9
+ import { User, Mail, Lock, Loader2, AlertCircle, CheckCircle } from 'lucide-svelte';
10
+
11
+ const identity = useGetIdentity();
12
+ const updatePw = useUpdatePassword();
13
+
14
+ // Password change form
15
+ let currentPassword = $state('');
16
+ let newPassword = $state('');
17
+ let confirmPassword = $state('');
18
+ let pwError = $state('');
19
+ let pwSuccess = $state(false);
20
+
21
+ async function handlePasswordChange(e: SubmitEvent) {
22
+ e.preventDefault();
23
+ pwError = '';
24
+ pwSuccess = false;
25
+
26
+ if (!newPassword) { pwError = t('auth.passwordRequired'); return; }
27
+ if (newPassword !== confirmPassword) { pwError = t('auth.passwordMismatch'); return; }
28
+
29
+ const result = await updatePw.mutate({
30
+ password: newPassword,
31
+ currentPassword,
32
+ confirmPassword,
33
+ });
34
+ if (result.success) {
35
+ pwSuccess = true;
36
+ currentPassword = '';
37
+ newPassword = '';
38
+ confirmPassword = '';
39
+ } else {
40
+ pwError = result.error?.message ?? t('common.operationFailed');
41
+ }
42
+ }
43
+
44
+ // Avatar initials
45
+ const initials = $derived(
46
+ identity.data?.name
47
+ ? identity.data.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
48
+ : '?'
49
+ );
50
+ </script>
51
+
52
+ <div class="max-w-2xl mx-auto space-y-6 p-4 sm:p-6">
53
+ <!-- Profile Info -->
54
+ <Card.Card>
55
+ <Card.CardHeader>
56
+ <Card.CardTitle class="flex items-center gap-2">
57
+ <User class="h-5 w-5" />
58
+ {t('profile.title')}
59
+ </Card.CardTitle>
60
+ </Card.CardHeader>
61
+ <Card.CardContent>
62
+ {#if identity.isLoading}
63
+ <div class="flex items-center gap-2 text-muted-foreground">
64
+ <Loader2 class="h-4 w-4 animate-spin" />
65
+ {t('common.loading')}
66
+ </div>
67
+ {:else if identity.data}
68
+ <div class="flex items-start gap-6">
69
+ <!-- Avatar -->
70
+ <div class="shrink-0">
71
+ {#if identity.data.avatar}
72
+ <img
73
+ src={identity.data.avatar}
74
+ alt={identity.data.name ?? ''}
75
+ class="h-20 w-20 rounded-xl object-cover ring-2 ring-primary/20"
76
+ />
77
+ {:else}
78
+ <div class="h-20 w-20 rounded-xl bg-primary/10 text-primary flex items-center justify-center text-2xl font-bold">
79
+ {initials}
80
+ </div>
81
+ {/if}
82
+ </div>
83
+
84
+ <!-- Info -->
85
+ <div class="space-y-3 flex-1 min-w-0">
86
+ <div>
87
+ <Label class="text-xs text-muted-foreground">{t('profile.name')}</Label>
88
+ <p class="text-lg font-semibold truncate">{identity.data.name ?? '—'}</p>
89
+ </div>
90
+
91
+ {#if identity.data.email}
92
+ <div class="flex items-center gap-2 text-sm text-muted-foreground">
93
+ <Mail class="h-4 w-4 shrink-0" />
94
+ <span class="truncate">{identity.data.email}</span>
95
+ </div>
96
+ {/if}
97
+
98
+ {#if identity.data.id}
99
+ <div class="text-xs text-muted-foreground/60 font-mono">
100
+ ID: {identity.data.id}
101
+ </div>
102
+ {/if}
103
+ </div>
104
+ </div>
105
+ {:else}
106
+ <p class="text-sm text-muted-foreground">{t('profile.notAvailable')}</p>
107
+ {/if}
108
+ </Card.CardContent>
109
+ </Card.Card>
110
+
111
+ <!-- Change Password -->
112
+ <Card.Card>
113
+ <Card.CardHeader>
114
+ <Card.CardTitle class="flex items-center gap-2">
115
+ <Lock class="h-5 w-5" />
116
+ {t('profile.changePassword')}
117
+ </Card.CardTitle>
118
+ </Card.CardHeader>
119
+ <Card.CardContent>
120
+ <form onsubmit={handlePasswordChange} class="space-y-4 max-w-sm">
121
+ {#if pwError}
122
+ <Alert.Root variant="destructive">
123
+ <AlertCircle class="h-4 w-4" />
124
+ <Alert.Description>{pwError}</Alert.Description>
125
+ </Alert.Root>
126
+ {/if}
127
+
128
+ {#if pwSuccess}
129
+ <Alert.Root class="border-green-500/30 bg-green-500/5 text-green-600 dark:text-green-400">
130
+ <CheckCircle class="h-4 w-4" />
131
+ <Alert.Description>{t('profile.passwordChanged')}</Alert.Description>
132
+ </Alert.Root>
133
+ {/if}
134
+
135
+ <PasswordInput
136
+ id="current-password"
137
+ label={t('profile.currentPassword')}
138
+ bind:value={currentPassword}
139
+ autocomplete="current-password"
140
+ />
141
+
142
+ <PasswordInput
143
+ id="new-password"
144
+ label={t('auth.password')}
145
+ bind:value={newPassword}
146
+ autocomplete="new-password"
147
+ showStrength
148
+ />
149
+
150
+ <PasswordInput
151
+ id="confirm-new-password"
152
+ label={t('auth.confirmPassword')}
153
+ bind:value={confirmPassword}
154
+ autocomplete="new-password"
155
+ />
156
+
157
+ <Button type="submit" disabled={updatePw.isLoading}>
158
+ {#if updatePw.isLoading}
159
+ <Loader2 class="h-4 w-4 animate-spin mr-2" />
160
+ {/if}
161
+ {t('profile.updatePassword')}
162
+ </Button>
163
+ </form>
164
+ </Card.CardContent>
165
+ </Card.Card>
166
+ </div>
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import Alert, { type AlertVariant, alertVariants } from "./alert.svelte";
2
3
  import AlertTitle from "./alert-title.svelte";
3
4
  import AlertDescription from "./alert-description.svelte";
@@ -1,2 +1,3 @@
1
+ // @ts-nocheck
1
2
  export { default as Badge } from "./badge.svelte";
2
3
  export { badgeVariants, type BadgeVariant } from "./badge.svelte";
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import Root, {
2
3
  type ButtonProps,
3
4
  type ButtonSize,
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export { default as DrawerForm } from './components/DrawerForm.svelte';
28
28
  export { default as DevTools } from './components/DevTools.svelte';
29
29
  export { default as Authenticated } from './components/Authenticated.svelte';
30
30
  export { default as UpdatePasswordPage } from './components/UpdatePasswordPage.svelte';
31
+ export { default as ProfilePage } from './components/ProfilePage.svelte';
31
32
  export { default as ConfigErrorScreen } from './components/ConfigErrorScreen.svelte';
32
33
  export { default as InferencerPanel } from './components/InferencerPanel.svelte';
33
34
  export { default as LiveIndicator } from './components/LiveIndicator.svelte';