@svadmin/core 0.19.1 → 0.19.3

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/core",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,7 +61,5 @@
61
61
  "sideEffects": false,
62
62
  "type": "module",
63
63
  "types": "src/index.ts",
64
- "dependencies": {
65
- "sonner-svelte": "^0.3.3"
66
- }
64
+ "dependencies": {}
67
65
  }
@@ -4,32 +4,42 @@
4
4
 
5
5
  import { getAuthProvider } from './context.svelte';
6
6
  import { navigate } from './router';
7
- import { toast } from './toast.svelte';
7
+ import { notify } from './notification.svelte';
8
8
  import { t } from './i18n.svelte';
9
- import type { AuthActionResult, CheckResult, Identity } from './types';
9
+ import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './types';
10
10
 
11
- // ─── useLogin ─────────────────────────────────────────────────
11
+ // ─── Mutate Factory ─────────────────────────────────────────────
12
12
 
13
- export function useLogin() {
13
+ interface CreateAuthMutationOptions {
14
+ method: keyof AuthProvider;
15
+ successMessage?: string | null;
16
+ errorMessage?: string;
17
+ onSuccess?: (result: AuthActionResult) => void;
18
+ }
19
+
20
+ function createAuthMutation(options: CreateAuthMutationOptions) {
14
21
  const provider = getAuthProvider();
15
22
  let isLoading = $state(false);
16
23
 
17
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
24
+ async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
18
25
  if (!provider) throw new Error('AuthProvider not configured');
26
+ const fn = provider[options.method] as ((params?: Record<string, unknown>) => Promise<AuthActionResult>) | undefined;
27
+ if (!fn) throw new Error(`AuthProvider.${options.method} not implemented`);
28
+
19
29
  isLoading = true;
20
30
  try {
21
- const result = await provider.login(params);
31
+ const result = await fn.call(provider, params);
22
32
  if (result.success) {
23
- toast.success(t('common.operationSuccess'));
24
- if (result.redirectTo) navigate(result.redirectTo);
33
+ if (options.successMessage) notify({ type: 'success', message: options.successMessage });
34
+ if (options.onSuccess) options.onSuccess(result);
25
35
  } else {
26
- const msg = result.error?.message ?? t('common.loginFailed');
27
- toast.error(msg);
36
+ const msg = result.error?.message ?? options.errorMessage ?? t('common.operationFailed');
37
+ notify({ type: 'error', message: msg });
28
38
  }
29
39
  return result;
30
40
  } catch (err) {
31
- const msg = err instanceof Error ? err.message : t('common.loginFailed');
32
- toast.error(msg);
41
+ const msg = err instanceof Error ? err.message : (options.errorMessage ?? t('common.operationFailed'));
42
+ notify({ type: 'error', message: msg });
33
43
  return { success: false, error: { message: msg } };
34
44
  } finally {
35
45
  isLoading = false;
@@ -42,135 +52,54 @@ export function useLogin() {
42
52
  };
43
53
  }
44
54
 
45
- // ─── useLogout ────────────────────────────────────────────────
55
+ // ─── useLogin ─────────────────────────────────────────────────
46
56
 
47
- export function useLogout() {
48
- const provider = getAuthProvider();
49
- let isLoading = $state(false);
57
+ export function useLogin() {
58
+ return createAuthMutation({
59
+ method: 'login',
60
+ successMessage: t('common.operationSuccess'),
61
+ errorMessage: t('common.loginFailed'),
62
+ onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
63
+ });
64
+ }
50
65
 
51
- async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
52
- if (!provider) throw new Error('AuthProvider not configured');
53
- isLoading = true;
54
- try {
55
- const result = await provider.logout(params);
56
- if (result.success) {
57
- navigate(result.redirectTo ?? '/login');
58
- }
59
- return result;
60
- } catch (err) {
61
- const msg = err instanceof Error ? err.message : t('common.operationFailed');
62
- toast.error(msg);
63
- return { success: false, error: { message: msg } };
64
- } finally {
65
- isLoading = false;
66
- }
67
- }
66
+ // ─── useLogout ────────────────────────────────────────────────
68
67
 
69
- return {
70
- mutate,
71
- get isLoading() { return isLoading; },
72
- };
68
+ export function useLogout() {
69
+ return createAuthMutation({
70
+ method: 'logout',
71
+ successMessage: null,
72
+ onSuccess: (result) => { navigate(result.redirectTo ?? '/login'); }
73
+ });
73
74
  }
74
75
 
75
76
  // ─── useRegister ──────────────────────────────────────────────
76
77
 
77
78
  export function useRegister() {
78
- const provider = getAuthProvider();
79
- let isLoading = $state(false);
80
-
81
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
82
- if (!provider?.register) throw new Error('AuthProvider.register not implemented');
83
- isLoading = true;
84
- try {
85
- const result = await provider.register(params);
86
- if (result.success) {
87
- toast.success(t('auth.registerSuccess'));
88
- if (result.redirectTo) navigate(result.redirectTo);
89
- } else {
90
- const msg = result.error?.message ?? t('common.operationFailed');
91
- toast.error(msg);
92
- }
93
- return result;
94
- } catch (err) {
95
- const msg = err instanceof Error ? err.message : t('common.operationFailed');
96
- toast.error(msg);
97
- return { success: false, error: { message: msg } };
98
- } finally {
99
- isLoading = false;
100
- }
101
- }
102
-
103
- return {
104
- mutate,
105
- get isLoading() { return isLoading; },
106
- };
79
+ return createAuthMutation({
80
+ method: 'register',
81
+ successMessage: t('auth.registerSuccess'),
82
+ onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
83
+ });
107
84
  }
108
85
 
109
86
  // ─── useForgotPassword ───────────────────────────────────────
110
87
 
111
88
  export function useForgotPassword() {
112
- const provider = getAuthProvider();
113
- let isLoading = $state(false);
114
-
115
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
116
- if (!provider?.forgotPassword) throw new Error('AuthProvider.forgotPassword not implemented');
117
- isLoading = true;
118
- try {
119
- const result = await provider.forgotPassword(params);
120
- if (result.success) {
121
- toast.success(t('auth.resetLinkSent'));
122
- } else {
123
- const msg = result.error?.message ?? t('common.operationFailed');
124
- toast.error(msg);
125
- }
126
- return result;
127
- } catch (err) {
128
- const msg = err instanceof Error ? err.message : t('common.operationFailed');
129
- toast.error(msg);
130
- return { success: false, error: { message: msg } };
131
- } finally {
132
- isLoading = false;
133
- }
134
- }
135
-
136
- return {
137
- mutate,
138
- get isLoading() { return isLoading; },
139
- };
89
+ return createAuthMutation({
90
+ method: 'forgotPassword',
91
+ successMessage: t('auth.resetLinkSent')
92
+ });
140
93
  }
141
94
 
142
95
  // ─── useUpdatePassword ───────────────────────────────────────
143
96
 
144
97
  export function useUpdatePassword() {
145
- const provider = getAuthProvider();
146
- let isLoading = $state(false);
147
-
148
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
149
- if (!provider?.updatePassword) throw new Error('AuthProvider.updatePassword not implemented');
150
- isLoading = true;
151
- try {
152
- const result = await provider.updatePassword(params);
153
- if (result.success) {
154
- toast.success(t('common.operationSuccess'));
155
- if (result.redirectTo) navigate(result.redirectTo);
156
- } else {
157
- const msg = result.error?.message ?? t('common.operationFailed');
158
- toast.error(msg);
159
- }
160
- return result;
161
- } catch (err) {
162
- const msg = err instanceof Error ? err.message : t('common.operationFailed');
163
- toast.error(msg);
164
- return { success: false, error: { message: msg } };
165
- } finally {
166
- isLoading = false;
167
- }
168
- }
169
-
170
- return {
171
- mutate,
172
- get isLoading() { return isLoading; },
173
- };
98
+ return createAuthMutation({
99
+ method: 'updatePassword',
100
+ successMessage: t('common.operationSuccess'),
101
+ onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
102
+ });
174
103
  }
175
104
 
176
105
  // ─── useGetIdentity ──────────────────────────────────────────
@@ -86,8 +86,7 @@ export function setResources(newResources: ResourceDefinition[]): void {
86
86
  }
87
87
 
88
88
  export function getResources(): ResourceDefinition[] {
89
- if (!resources || resources.length === 0) throw new Error('Resources not found. Did you call setResources in App.svelte?');
90
- return resources;
89
+ return resources ?? [];
91
90
  }
92
91
 
93
92
  export function getResource(nameOrIdentifier: string): ResourceDefinition {
@@ -3,7 +3,7 @@ import { useParsed } from './useParsed.svelte';
3
3
  import { getAdminOptions } from './options.svelte';
4
4
  import { getDataProviderForResource } from './context.svelte';
5
5
  import { createQuery, createMutation } from '@tanstack/svelte-query';
6
- import { toast } from './toast.svelte';
6
+ import { notify } from './notification.svelte';
7
7
  import { t } from './i18n.svelte';
8
8
  import { audit } from './audit';
9
9
  import { navigate } from './router';
@@ -245,9 +245,9 @@ export function useForm<
245
245
  const msg = Array.isArray(messages) ? messages[0] : messages;
246
246
  setFieldError(field, msg);
247
247
  }
248
- if (errorNotification !== false) toast.error(errorNotification || error.message || t('common.operationFailed'));
248
+ if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (error.message || t('common.operationFailed')) });
249
249
  } else {
250
- if (errorNotification !== false) toast.error(errorNotification || t('common.operationFailed') + ': ' + error.message);
250
+ if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (t('common.operationFailed') + ': ' + error.message) });
251
251
  }
252
252
  }
253
253
 
@@ -288,7 +288,7 @@ export function useForm<
288
288
  mutationFn: (variables: TVariables) => provider.create<TData, TVariables>({ resource, variables, meta: mutationMeta }),
289
289
  onSuccess: (data: { data: TData }) => {
290
290
  if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
291
- if (successNotification !== false) toast.success(successNotification || t('common.createSuccess'));
291
+ if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.createSuccess') });
292
292
  audit({ action: 'create', resource, recordId: String((data.data as Record<string, unknown>).id) });
293
293
  onMutationSuccess?.(data);
294
294
  if (redirectOverride !== false) doRedirect(redirectOverride ?? redirectDefault);
@@ -301,7 +301,7 @@ export function useForm<
301
301
  mutationFn: (variables: TVariables) => provider.update<TData, TVariables>({ resource, id: currentId!, variables, meta: mutationMeta }),
302
302
  onSuccess: (data: { data: TData }) => {
303
303
  if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
304
- if (successNotification !== false) toast.success(successNotification || t('common.updateSuccess'));
304
+ if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.updateSuccess') });
305
305
  audit({ action: 'update', resource, recordId: String(currentId) });
306
306
  onMutationSuccess?.(data);
307
307
  if (redirectOverride !== false) doRedirect(redirectOverride ?? redirectDefault);
@@ -324,7 +324,7 @@ export function useForm<
324
324
  if (result === false || cancelled) return;
325
325
  }
326
326
 
327
- if (!runValidation()) { toast.warning(t('validation.required')); return; }
327
+ if (!runValidation()) { notify({ type: 'warning', message: t('validation.required') }); return; }
328
328
  redirectOverride = overrides?.redirect;
329
329
  try {
330
330
  if (action === 'create' || action === 'clone') await createMut.mutateAsync(values);
@@ -334,7 +334,6 @@ export function useForm<
334
334
  } catch (error) {
335
335
  // Errors are already handled by the mutation's onError callback.
336
336
  // We catch here to prevent unhandled promise rejections.
337
- handleHttpError(error instanceof Error ? error : new Error(String(error)));
338
337
  }
339
338
  }
340
339
 
@@ -477,6 +477,7 @@ const locales: Record<string, Locale> = {
477
477
 
478
478
  /** Detect best locale from browser language */
479
479
  function detectLocale(): string {
480
+ if (typeof navigator === 'undefined') return 'en'; // Safe default for SSR
480
481
  const browserLang = navigator.language || navigator.languages?.[0] || 'zh-CN';
481
482
  // Exact match
482
483
  if (locales[browserLang]) return browserLang;
package/src/index.ts CHANGED
@@ -29,7 +29,7 @@ export { matchRoute, navigate, currentPath, setActiveRouterProvider } from './ro
29
29
  export { readURLState, writeURLState } from './url-sync';
30
30
  export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
31
31
  export { useLive, useSubscription, usePublish } from './live.svelte';
32
- export { toast } from './toast.svelte';
32
+ export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue } from './toast.svelte';
33
33
  export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
34
34
  export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation } from './i18n.svelte';
35
35
  export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
@@ -27,7 +27,7 @@ export interface ColorPreset {
27
27
  }
28
28
 
29
29
  /** Built-in color presets — consumers can extend via registerColorPreset(). */
30
- export const builtinPresets: Record<string, ColorPreset> = {
30
+ export const builtinPresets: Record<string, ColorPreset> = $state({
31
31
  neutral: {
32
32
  name: 'neutral',
33
33
  label: 'Neutral',
@@ -189,7 +189,7 @@ export const builtinPresets: Record<string, ColorPreset> = {
189
189
  '--chart-1': 'oklch(0.541 0.281 293.009)',
190
190
  },
191
191
  },
192
- };
192
+ });
193
193
 
194
194
  /** Register a custom color preset. Overwrites any built-in preset with the same name. */
195
195
  export function registerColorPreset(preset: ColorPreset): void {
@@ -292,11 +292,11 @@ export function clearCssOverrides(keys?: string[]): void {
292
292
  }
293
293
 
294
294
  // ── Color themes (display metadata, derived from presets) ──
295
- export const colorThemes: { id: ColorTheme | string; label: string; color: string }[] = Object.values(builtinPresets).map(p => ({
295
+ export const colorThemes: { id: ColorTheme | string; label: string; color: string }[] = $state(Object.values(builtinPresets).map(p => ({
296
296
  id: p.name as ColorTheme,
297
297
  label: p.label,
298
298
  color: p.color,
299
- }));
299
+ })));
300
300
 
301
301
  // ── Dark/Light mode ──────────────────────────────────────
302
302
 
@@ -1,40 +1,40 @@
1
- // Toast notification system — wraps sonner-svelte for consistent API
2
- // Maintains backward-compatible `toast.success()` / `toast.error()` etc.
3
-
4
- import { toast as sonner } from 'sonner-svelte';
5
-
6
1
  export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
7
2
 
8
- export interface Toast {
3
+ export interface ToastItem {
9
4
  id: number;
10
5
  type: ToastType;
11
6
  message: string;
12
- duration: number;
7
+ duration?: number;
13
8
  onUndo?: () => void;
14
9
  onTimeout?: () => void;
15
10
  }
16
11
 
17
- // Legacy compatibility — kept for existing consumers
18
12
  let nextId = 0;
19
- let toasts = $state<Toast[]>([]);
20
13
 
21
- export function getToasts(): Toast[] {
22
- return toasts;
23
- }
24
-
25
- export function addToast(type: ToastType, message: string, duration = 3000, options?: { onUndo?: () => void, onTimeout?: () => void }): void {
26
- // Use sonner-svelte for regular toasts
14
+ // Queue for simple toasts (consumed by UI Toast bridge)
15
+ let queue = $state<ToastItem[]>([]);
16
+ export function getToastQueue() { return queue; }
17
+ export function consumeToastQueue() { queue = []; }
18
+
19
+ // Promise Queue
20
+ let promiseQueue = $state<{ id: number; promise: Promise<any>; opts: { loading: string; success: string; error: string } }[]>([]);
21
+ export function getPromiseQueue() { return promiseQueue; }
22
+ export function consumePromiseQueue() { promiseQueue = []; }
23
+
24
+ // Legacy compatibility — kept for existing consumers (Undoable)
25
+ let toasts = $state<ToastItem[]>([]);
26
+ export function getToasts(): ToastItem[] { return toasts; }
27
+
28
+ export function addToast(
29
+ type: ToastType,
30
+ message: string,
31
+ duration = 3000,
32
+ options?: { onUndo?: () => void, onTimeout?: () => void }
33
+ ): void {
27
34
  if (type !== 'undoable') {
28
- switch (type) {
29
- case 'success': sonner.success(message, { duration }); break;
30
- case 'error': sonner.error(message, { duration }); break;
31
- case 'warning': sonner.warning(message, { duration }); break;
32
- case 'info': sonner.info(message, { duration }); break;
33
- }
35
+ queue.push({ id: nextId++, type, message, duration });
34
36
  return;
35
37
  }
36
-
37
- // Undoable toasts still use custom system
38
38
  const id = nextId++;
39
39
  toasts = [...toasts, { id, type, message, duration, ...options }];
40
40
  }
@@ -52,6 +52,8 @@ export const toast = {
52
52
  undoable: (msg: string, duration: number, onUndo: () => void, onTimeout: () => void) =>
53
53
  addToast('undoable', msg, duration, { onUndo, onTimeout }),
54
54
  /** Promise toast — shows loading → success/error automatically */
55
- promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) =>
56
- sonner.promise(promise, opts),
55
+ promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) => {
56
+ promiseQueue.push({ id: nextId++, promise, opts });
57
+ return promise;
58
+ },
57
59
  };