@svadmin/ui 0.42.0 → 0.42.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.
@@ -200,8 +200,7 @@
200
200
  const routerState = provideRouterState(untrack(() => resolvedRouter));
201
201
  const scopedRouterProvider: RouterProvider = {
202
202
  go(options) {
203
- resolvedRouter.go(options);
204
- routerState.sync();
203
+ return navigateAfterAuthCheck(options);
205
204
  },
206
205
  back() {
207
206
  resolvedRouter.back();
@@ -303,9 +302,8 @@
303
302
  // Reactive getters for route state
304
303
  const route = $derived(routerState.route);
305
304
  const params = $derived(routerState.params);
305
+ const path = $derived(routerState.path);
306
306
  const resourceNames = $derived(new Set(resources.map((resource) => resource.name)));
307
- const hasRouteResource = $derived(!params.resource || resourceNames.has(params.resource));
308
- const currentResourcePages = $derived(params.resource ? resourcePages?.[params.resource] : undefined);
309
307
  const isStandaloneTwoFactor = $derived(route === '/2fa' || route === '/authentication/branded/2fa');
310
308
  const standaloneErrorStatus = $derived.by((): '404' | '500' | undefined => {
311
309
  if (route === '/authentication/error-404' || route === '/404' || (route === '/:resource' && params.resource === '404')) return '404';
@@ -316,9 +314,82 @@
316
314
  // Auth check
317
315
  let isAuthenticated = $state(untrack(() => !resolvedAuthProvider));
318
316
  let authChecked = $state(untrack(() => !resolvedAuthProvider));
317
+ let checkedAuthProvider = $state.raw<AuthProvider | null>(null);
318
+ let checkedAuthPath = $state<string | null>(null);
319
+ let navigationAuthPending = $state(false);
320
+ let navigationCheckId = 0;
321
+ const authProviderPending = $derived(!!resolvedAuthProvider && checkedAuthProvider !== resolvedAuthProvider);
322
+ const authRechecking = $derived(
323
+ navigationAuthPending
324
+ || (!!resolvedAuthProvider
325
+ && !authProviderPending
326
+ && authChecked
327
+ && isAuthenticated
328
+ && checkedAuthPath !== path),
329
+ );
330
+ const renderedRoute = $derived(route);
331
+ const renderedParams = $derived(params);
332
+ const renderedHasRouteResource = $derived(!renderedParams.resource || resourceNames.has(renderedParams.resource));
333
+ const renderedResourcePages = $derived(renderedParams.resource ? resourcePages?.[renderedParams.resource] : undefined);
334
+
335
+ async function navigateWithinApp(path: string, options?: { replaceState?: boolean }) {
336
+ await adminContext.navigate(path, options);
337
+ }
338
+
339
+ async function commitNavigation(options: Parameters<RouterProvider['go']>[0]): Promise<Awaited<ReturnType<RouterProvider['go']>>> {
340
+ const navigated = await resolvedRouter.go(options);
341
+ if (navigated === false) return false;
342
+ routerState.sync();
343
+ return navigated;
344
+ }
345
+
346
+ function recordCheckedRoute(provider: AuthProvider) {
347
+ checkedAuthProvider = provider;
348
+ checkedAuthPath = routerState.path;
349
+ }
350
+
351
+ async function navigateAfterAuthCheck(options: Parameters<RouterProvider['go']>[0]): Promise<Awaited<ReturnType<RouterProvider['go']>>> {
352
+ const provider = resolvedAuthProvider;
353
+ if (!provider || checkedAuthProvider !== provider || !authChecked || !isAuthenticated) {
354
+ return commitNavigation(options);
355
+ }
356
+
357
+ const checkId = ++navigationCheckId;
358
+ navigationAuthPending = true;
359
+ let result: Awaited<ReturnType<AuthProvider['check']>>;
360
+ try {
361
+ result = await provider.check();
362
+ } catch (err) {
363
+ if (checkId !== navigationCheckId || provider !== resolvedAuthProvider) return false;
364
+ console.warn('Auth check failed:', err);
365
+ isAuthenticated = false;
366
+ authChecked = true;
367
+ try {
368
+ await navigateWithinApp('/login', { replaceState: true });
369
+ recordCheckedRoute(provider);
370
+ return false;
371
+ } finally {
372
+ if (checkId === navigationCheckId) navigationAuthPending = false;
373
+ }
374
+ }
319
375
 
320
- async function navigateWithinApp(path: string) {
321
- await adminContext.navigate(path);
376
+ if (checkId !== navigationCheckId || provider !== resolvedAuthProvider) return false;
377
+ try {
378
+ if (!result.authenticated) {
379
+ isAuthenticated = false;
380
+ authChecked = true;
381
+ await navigateWithinApp(result.redirectTo ?? '/login', { replaceState: true });
382
+ recordCheckedRoute(provider);
383
+ return false;
384
+ }
385
+ const navigated = await commitNavigation(options);
386
+ if (navigated === false) return false;
387
+ if (checkId !== navigationCheckId || provider !== resolvedAuthProvider) return false;
388
+ recordCheckedRoute(provider);
389
+ return navigated;
390
+ } finally {
391
+ if (checkId === navigationCheckId) navigationAuthPending = false;
392
+ }
322
393
  }
323
394
 
324
395
  function isPublicSystemRoute(currentRoute: string): boolean {
@@ -332,19 +403,35 @@
332
403
  let cancelled = false;
333
404
  // Explicitly track route so auth is re-checked on navigation
334
405
  const _route = route;
406
+ const _path = path;
407
+ const provider = resolvedAuthProvider;
408
+ navigationCheckId += 1;
409
+ navigationAuthPending = false;
335
410
 
336
- if (!resolvedAuthProvider) {
411
+ if (!provider) {
337
412
  if (!cancelled) {
338
413
  isAuthenticated = true;
339
414
  authChecked = true;
415
+ checkedAuthProvider = null;
416
+ checkedAuthPath = _path;
340
417
  }
341
418
  return;
342
419
  }
343
- authChecked = false;
344
- resolvedAuthProvider.check().then(result => {
420
+ if (
421
+ untrack(() => checkedAuthProvider) === provider
422
+ && untrack(() => checkedAuthPath) === _path
423
+ ) return;
424
+ const providerChanged = untrack(() => checkedAuthProvider) !== provider;
425
+ if (providerChanged) {
426
+ isAuthenticated = false;
427
+ authChecked = false;
428
+ }
429
+ provider.check().then(result => {
345
430
  if (cancelled) return;
346
431
  isAuthenticated = result.authenticated;
347
432
  authChecked = true;
433
+ checkedAuthProvider = provider;
434
+ checkedAuthPath = _path;
348
435
  if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password' && !isPublicSystemRoute(_route)) {
349
436
  void navigateWithinApp(result.redirectTo ?? '/login');
350
437
  }
@@ -353,6 +440,8 @@
353
440
  console.warn('Auth check failed:', err);
354
441
  isAuthenticated = false;
355
442
  authChecked = true;
443
+ checkedAuthProvider = provider;
444
+ checkedAuthPath = _path;
356
445
  if (_route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password' && !isPublicSystemRoute(_route)) {
357
446
  void navigateWithinApp('/login');
358
447
  }
@@ -365,7 +454,7 @@
365
454
  <QueryClientProvider client={queryClient}>
366
455
  {#if !resolvedDataProvider}
367
456
  <ConfigErrorScreen title="{title} — DataProvider is required" />
368
- {:else if !authChecked}
457
+ {:else if !authChecked || authProviderPending}
369
458
  <div class="flex h-screen items-center justify-center">
370
459
  <div class="space-y-4 text-center">
371
460
  <div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary mx-auto"></div>
@@ -390,7 +479,7 @@
390
479
  <UpdatePasswordPage {title} />
391
480
  {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
392
481
  <ConfigErrorScreen title="{title} — {translation.t('common.configRequired')}" />
393
- {:else if (isAuthenticated || !resolvedAuthProvider) && isStandaloneTwoFactor}
482
+ {:else if (isAuthenticated || !resolvedAuthProvider) && isStandaloneTwoFactor && !authRechecking}
394
483
  <SystemPageShell {title}>
395
484
  <LazyPage loader={loadTwoFactorAuthPage} props={{}} />
396
485
  </SystemPageShell>
@@ -401,47 +490,56 @@
401
490
  </SystemPageShell>
402
491
  {:else if isAuthenticated || !resolvedAuthProvider}
403
492
  <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
404
- {#key route + (params.resource ?? '') + (params.id ?? '') + (params.variant ?? '') + (params.columns ?? '')}
493
+ <div class="relative min-h-40" aria-busy={authRechecking}>
494
+ {#if authRechecking}
495
+ <div class="absolute inset-0 z-10 flex min-h-40 items-center justify-center bg-background/80" role="status" aria-live="polite">
496
+ <div class="space-y-3 text-center">
497
+ <div class="h-6 w-6 animate-spin rounded-full border-4 border-muted border-t-primary mx-auto"></div>
498
+ <p class="text-sm text-muted-foreground">Loading...</p>
499
+ </div>
500
+ </div>
501
+ {:else}
502
+ {#key renderedRoute + (renderedParams.resource ?? '') + (renderedParams.id ?? '') + (renderedParams.variant ?? '') + (renderedParams.columns ?? '')}
405
503
  <div class="svadmin-page-enter">
406
- {#if route === '/public-profile'}
504
+ {#if renderedRoute === '/public-profile'}
407
505
  <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects' }} />
408
- {:else if route === '/public-profile/projects/:columns'}
409
- <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects', columns: params.columns?.includes('3') ? 3 : 2 }} />
410
- {:else if route === '/public-profile/activity'}
506
+ {:else if renderedRoute === '/public-profile/projects/:columns'}
507
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects', columns: renderedParams.columns?.includes('3') ? 3 : 2 }} />
508
+ {:else if renderedRoute === '/public-profile/activity'}
411
509
  <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'activity' }} />
412
- {:else if route === '/public-profile/teams'}
510
+ {:else if renderedRoute === '/public-profile/teams'}
413
511
  <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'teams' }} />
414
- {:else if route === '/public-profile/profiles/:variant'}
415
- <LazyPage loader={loadPublicProfilePage} props={{ variant: (params.variant === 'company' || params.variant === 'gamer' || params.variant === 'default' ? params.variant : 'default') as 'company' | 'gamer' | 'default', showSections: true }} />
416
- {:else if route === '/account/get-started' || route === '/account/home/get-started'}
512
+ {:else if renderedRoute === '/public-profile/profiles/:variant'}
513
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: (renderedParams.variant === 'company' || renderedParams.variant === 'gamer' || renderedParams.variant === 'default' ? renderedParams.variant : 'default') as 'company' | 'gamer' | 'default', showSections: true }} />
514
+ {:else if renderedRoute === '/account/get-started' || renderedRoute === '/account/home/get-started'}
417
515
  <LazyPage loader={loadGetStartedPage} props={{}} />
418
- {:else if route === '/account/home/user-profile'}
516
+ {:else if renderedRoute === '/account/home/user-profile'}
419
517
  <LazyPage loader={loadUserProfilePage} props={{}} />
420
- {:else if route === '/account/company-profile' || route === '/account/home/company-profile'}
518
+ {:else if renderedRoute === '/account/company-profile' || renderedRoute === '/account/home/company-profile'}
421
519
  <LazyPage loader={loadCompanyProfilePage} props={{}} />
422
- {:else if route === '/account/settings-plain' || route === '/account/home/settings-plain'}
520
+ {:else if renderedRoute === '/account/settings-plain' || renderedRoute === '/account/home/settings-plain'}
423
521
  <LazyPage loader={loadSettingsPlainPage} props={{}} />
424
- {:else if route === '/account/settings-sidebar' || route === '/account/home/settings-sidebar'}
522
+ {:else if renderedRoute === '/account/settings-sidebar' || renderedRoute === '/account/home/settings-sidebar'}
425
523
  <LazyPage loader={loadSettingsSidebarPage} props={{}} />
426
- {:else if route === '/account/settings-enterprise' || route === '/account/home/settings-enterprise'}
524
+ {:else if renderedRoute === '/account/settings-enterprise' || renderedRoute === '/account/home/settings-enterprise'}
427
525
  <LazyPage loader={loadSettingsEnterprisePage} props={{}} />
428
- {:else if route === '/account/:tab'}
526
+ {:else if renderedRoute === '/account/:tab'}
429
527
  <SettingsPage />
430
- {:else if route === '/account/import-members' || route === '/account/members/import-members'}
528
+ {:else if renderedRoute === '/account/import-members' || renderedRoute === '/account/members/import-members'}
431
529
  <LazyPage loader={loadImportMembersPage} props={{}} />
432
- {:else if route === '/account/members-starter' || route === '/account/members/members-starter'}
530
+ {:else if renderedRoute === '/account/members-starter' || renderedRoute === '/account/members/members-starter'}
433
531
  <LazyPage loader={loadMembersStarterPage} props={{}} />
434
- {:else if route === '/account/team-members' || route === '/account/members/team-members'}
532
+ {:else if renderedRoute === '/account/team-members' || renderedRoute === '/account/members/team-members'}
435
533
  <LazyPage loader={loadTeamMembersPage} props={{}} />
436
- {:else if route === '/account/security-log' || route === '/account/security/security-log'}
534
+ {:else if renderedRoute === '/account/security-log' || renderedRoute === '/account/security/security-log'}
437
535
  <LazyPage loader={loadSecurityLogPage} props={{}} />
438
- {:else if route === '/network/user-cards' || route === '/network/user-cards/nft'}
536
+ {:else if renderedRoute === '/network/user-cards' || renderedRoute === '/network/user-cards/nft'}
439
537
  <LazyPage loader={loadUserCardsNFTPage} props={{}} />
440
- {:else if route === '/network/team-crew' || route === '/network/user-table/team-crew'}
538
+ {:else if renderedRoute === '/network/team-crew' || renderedRoute === '/network/user-table/team-crew'}
441
539
  <LazyPage loader={loadTeamCrewTablePage} props={{}} />
442
- {:else if route.startsWith('/settings')}
540
+ {:else if renderedRoute.startsWith('/settings')}
443
541
  <SettingsPage />
444
- {:else if route === '/' || route === '' || route === '/inventory-dashboard'}
542
+ {:else if renderedRoute === '/' || renderedRoute === '' || renderedRoute === '/inventory-dashboard'}
445
543
  {#if dashboard}
446
544
  {@render dashboard()}
447
545
  {:else}
@@ -450,30 +548,30 @@
450
548
  <p class="text-muted-foreground">{translation.t('common.dashboardHint')}</p>
451
549
  </div>
452
550
  {/if}
453
- {:else if (route === '/:resource' || route === '/:parent/:parentId/:resource') && hasRouteResource}
454
- {#key params.resource}
455
- {@const Comp = currentResourcePages?.list ?? mergedComponents.AutoTable}
456
- <Comp resourceName={params.resource} />
551
+ {:else if (renderedRoute === '/:resource' || renderedRoute === '/:parent/:parentId/:resource') && renderedHasRouteResource}
552
+ {#key renderedParams.resource}
553
+ {@const Comp = renderedResourcePages?.list ?? mergedComponents.AutoTable}
554
+ <Comp resourceName={renderedParams.resource} />
457
555
  {/key}
458
- {:else if (route === '/:resource/create' || route === '/:parent/:parentId/:resource/create') && hasRouteResource}
459
- {#key params.resource}
460
- {@const Comp = currentResourcePages?.create ?? mergedComponents.AutoForm}
461
- <Comp resourceName={params.resource} mode="create" />
556
+ {:else if (renderedRoute === '/:resource/create' || renderedRoute === '/:parent/:parentId/:resource/create') && renderedHasRouteResource}
557
+ {#key renderedParams.resource}
558
+ {@const Comp = renderedResourcePages?.create ?? mergedComponents.AutoForm}
559
+ <Comp resourceName={renderedParams.resource} mode="create" />
462
560
  {/key}
463
- {:else if (route === '/:resource/edit/:id' || route === '/:parent/:parentId/:resource/edit/:id') && hasRouteResource}
464
- {#key `${params.resource}-${params.id}`}
465
- {@const Comp = currentResourcePages?.edit ?? mergedComponents.AutoForm}
466
- <Comp resourceName={params.resource} mode="edit" id={params.id} />
561
+ {:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource}
562
+ {#key `${renderedParams.resource}-${renderedParams.id}`}
563
+ {@const Comp = renderedResourcePages?.edit ?? mergedComponents.AutoForm}
564
+ <Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
467
565
  {/key}
468
- {:else if (route === '/:resource/show/:id' || route === '/:parent/:parentId/:resource/show/:id') && hasRouteResource}
469
- {#key `${params.resource}-${params.id}`}
470
- {@const Comp = currentResourcePages?.show ?? mergedComponents.ShowPage}
471
- <Comp resourceName={params.resource} id={params.id} />
566
+ {:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource}
567
+ {#key `${renderedParams.resource}-${renderedParams.id}`}
568
+ {@const Comp = renderedResourcePages?.show ?? mergedComponents.ShowPage}
569
+ <Comp resourceName={renderedParams.resource} id={renderedParams.id} />
472
570
  {/key}
473
- {:else if (route === '/:resource/clone/:id' || route === '/:parent/:parentId/:resource/clone/:id') && hasRouteResource}
474
- {#key `${params.resource}-clone-${params.id}`}
475
- {@const Comp = currentResourcePages?.clone ?? mergedComponents.AutoForm}
476
- <Comp resourceName={params.resource} mode="clone" id={params.id} />
571
+ {:else if (renderedRoute === '/:resource/clone/:id' || renderedRoute === '/:parent/:parentId/:resource/clone/:id') && renderedHasRouteResource}
572
+ {#key `${renderedParams.resource}-clone-${renderedParams.id}`}
573
+ {@const Comp = renderedResourcePages?.clone ?? mergedComponents.AutoForm}
574
+ <Comp resourceName={renderedParams.resource} mode="clone" id={renderedParams.id} />
477
575
  {/key}
478
576
  {:else}
479
577
  {@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
@@ -481,6 +579,8 @@
481
579
  {/if}
482
580
  </div>
483
581
  {/key}
582
+ {/if}
583
+ </div>
484
584
  </Layout>
485
585
  {:else}
486
586
  <div class="flex h-screen items-center justify-center">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.42.0",
3
+ "version": "0.42.2",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -39,7 +39,7 @@
39
39
  "./components/*": "./dist/components/*"
40
40
  },
41
41
  "peerDependencies": {
42
- "@svadmin/core": "^0.36.0",
42
+ "@svadmin/core": "^0.36.3",
43
43
  "@tanstack/svelte-query": "^6.1.38",
44
44
  "highlight.js": "^11.11.1",
45
45
  "isomorphic-dompurify": "^3.22.0",
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "dependencies": {
77
77
  "@floating-ui/dom": "^1.8.0",
78
- "@lucide/svelte": "^1.30.0",
78
+ "@lucide/svelte": "^1.31.0",
79
79
  "@tanstack/svelte-store": "^0.12.1",
80
80
  "@tanstack/svelte-table": "9.1.2",
81
81
  "@tanstack/table-core": "9.1.2",
@@ -89,19 +89,19 @@
89
89
  "author": "zuohuadong",
90
90
  "repository": {
91
91
  "type": "git",
92
- "url": "https://github.com/zuohuadong/svadmin.git",
92
+ "url": "https://github.com/vibeunion/svadmin.git",
93
93
  "directory": "packages/ui"
94
94
  },
95
- "homepage": "https://github.com/zuohuadong/svadmin#readme",
95
+ "homepage": "https://github.com/vibeunion/svadmin#readme",
96
96
  "bugs": {
97
- "url": "https://github.com/zuohuadong/svadmin/issues"
97
+ "url": "https://github.com/vibeunion/svadmin/issues"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@happy-dom/global-registrator": "^20.11.2",
101
101
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
102
102
  "@testing-library/dom": "^10.4.1",
103
103
  "@testing-library/svelte": "^5.4.2",
104
- "@testing-library/user-event": "^14.6.3",
104
+ "@testing-library/user-event": "^14.6.4",
105
105
  "happy-dom": "^20.11.2",
106
106
  "vitest": "^4.1.10"
107
107
  },