@svadmin/ui 0.0.3 → 0.3.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.
Files changed (54) hide show
  1. package/package.json +3 -2
  2. package/src/components/AdminApp.svelte +31 -21
  3. package/src/components/Authenticated.svelte +57 -0
  4. package/src/components/AutoForm.svelte +5 -5
  5. package/src/components/AutoSaveIndicator.svelte +30 -0
  6. package/src/components/AutoTable.svelte +81 -10
  7. package/src/components/Breadcrumbs.svelte +2 -0
  8. package/src/components/CatchAllNavigate.svelte +21 -0
  9. package/src/components/ConfigErrorScreen.svelte +224 -0
  10. package/src/components/ConfirmDialog.svelte +1 -1
  11. package/src/components/CreatePage.svelte +42 -0
  12. package/src/components/DevTools.svelte +83 -34
  13. package/src/components/EditPage.svelte +52 -0
  14. package/src/components/EmptyState.svelte +17 -31
  15. package/src/components/ErrorComponent.svelte +35 -0
  16. package/src/components/ForgotPasswordPage.svelte +12 -22
  17. package/src/components/Header.svelte +28 -59
  18. package/src/components/InferencerPanel.svelte +166 -0
  19. package/src/components/ListPage.svelte +47 -0
  20. package/src/components/LiveIndicator.svelte +115 -0
  21. package/src/components/LoginPage.svelte +13 -22
  22. package/src/components/NavigateToResource.svelte +20 -0
  23. package/src/components/PageSkeleton.svelte +70 -0
  24. package/src/components/RegisterPage.svelte +12 -23
  25. package/src/components/ShowPage.svelte +3 -3
  26. package/src/components/Sidebar.svelte +29 -10
  27. package/src/components/StatsCard.svelte +23 -2
  28. package/src/components/ThemedTitle.svelte +32 -0
  29. package/src/components/Toast.svelte +21 -11
  30. package/src/components/UpdatePasswordPage.svelte +217 -0
  31. package/src/components/buttons/CloneButton.svelte +30 -0
  32. package/src/components/buttons/CreateButton.svelte +29 -0
  33. package/src/components/buttons/DeleteButton.svelte +59 -0
  34. package/src/components/buttons/EditButton.svelte +30 -0
  35. package/src/components/buttons/ExportButton.svelte +24 -0
  36. package/src/components/buttons/ImportButton.svelte +50 -0
  37. package/src/components/buttons/ListButton.svelte +23 -0
  38. package/src/components/buttons/RefreshButton.svelte +31 -0
  39. package/src/components/buttons/SaveButton.svelte +27 -0
  40. package/src/components/buttons/ShowButton.svelte +24 -0
  41. package/src/components/buttons/index.ts +10 -0
  42. package/src/components/fields/BooleanField.svelte +13 -0
  43. package/src/components/fields/DateField.svelte +6 -0
  44. package/src/components/fields/EmailField.svelte +15 -0
  45. package/src/components/fields/FileField.svelte +19 -0
  46. package/src/components/fields/ImageField.svelte +14 -0
  47. package/src/components/fields/MarkdownField.svelte +19 -0
  48. package/src/components/fields/NumberField.svelte +6 -0
  49. package/src/components/fields/TagField.svelte +17 -0
  50. package/src/components/fields/TextField.svelte +16 -0
  51. package/src/components/fields/UrlField.svelte +20 -0
  52. package/src/components/ui/avatar/avatar.svelte +2 -1
  53. package/src/index.ts +32 -0
  54. package/src/router-state.svelte.ts +33 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.0.3",
3
+ "version": "0.3.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -21,7 +21,8 @@
21
21
  },
22
22
  "peerDependencies": {
23
23
  "svelte": "^5.0.0",
24
- "@svadmin/core": "0.1.0",
24
+ "@svadmin/core": "^0.3.0",
25
+ "@tanstack/svelte-query": "^5.0.0",
25
26
  "bits-ui": "^2.0.0",
26
27
  "tailwind-variants": "^3.0.0",
27
28
  "clsx": "^2.0.0",
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode } from '@svadmin/core';
4
- import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme } from '@svadmin/core';
3
+ import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider } from '@svadmin/core';
4
+ import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider } from '@svadmin/core';
5
+ import { t } from '@svadmin/core/i18n';
5
6
  import { navigate } from '@svadmin/core/router';
6
7
  import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
7
8
  import Layout from './Layout.svelte';
@@ -12,12 +13,15 @@
12
13
  import LoginPage from './LoginPage.svelte';
13
14
  import RegisterPage from './RegisterPage.svelte';
14
15
  import ForgotPasswordPage from './ForgotPasswordPage.svelte';
16
+ import UpdatePasswordPage from './UpdatePasswordPage.svelte';
17
+ import ConfigErrorScreen from './ConfigErrorScreen.svelte';
15
18
  import DevTools from './DevTools.svelte';
16
19
  import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
17
20
 
18
21
  interface Props {
19
22
  dataProvider: DataProvider;
20
23
  authProvider?: AuthProvider;
24
+ routerProvider?: RouterProvider;
21
25
  resources: ResourceDefinition[];
22
26
  locale?: string;
23
27
  title?: string;
@@ -29,6 +33,7 @@
29
33
  let {
30
34
  dataProvider,
31
35
  authProvider,
36
+ routerProvider,
32
37
  resources,
33
38
  locale,
34
39
  title = 'Admin',
@@ -37,10 +42,14 @@
37
42
  loginPage,
38
43
  }: Props = $props();
39
44
 
45
+ // Resolve router provider (default to hash)
46
+ const resolvedRouter = routerProvider ?? createHashRouterProvider();
47
+
40
48
  // Set up context
41
49
  setDataProvider(dataProvider);
42
50
  if (authProvider) setAuthProvider(authProvider);
43
51
  setResources(resources);
52
+ setRouterProvider(resolvedRouter);
44
53
  if (locale) setLocale(locale);
45
54
  if (defaultTheme) setTheme(defaultTheme);
46
55
 
@@ -50,23 +59,27 @@
50
59
  },
51
60
  });
52
61
 
53
- // Initialize hash router
54
- initRouter();
62
+ // Initialize router with provider
63
+ initRouter(resolvedRouter);
55
64
 
56
65
  // Reactive getters for route state
57
66
  const route = $derived(getRoute());
58
67
  const params = $derived(getParams());
59
68
 
60
69
  // Auth check
61
- let isAuthenticated = $state(!authProvider);
62
- let authChecked = $state(!authProvider);
70
+ let isAuthenticated = $state(false);
71
+ let authChecked = $state(false);
63
72
 
64
73
  $effect(() => {
65
- if (!authProvider) return;
74
+ if (!authProvider) {
75
+ isAuthenticated = true;
76
+ authChecked = true;
77
+ return;
78
+ }
66
79
  authProvider.check().then(result => {
67
80
  isAuthenticated = result.authenticated;
68
81
  authChecked = true;
69
- if (!result.authenticated && route !== '/login' && route !== '/register' && route !== '/forgot-password') {
82
+ if (!result.authenticated && route !== '/login' && route !== '/register' && route !== '/forgot-password' && route !== '/update-password') {
70
83
  navigate(result.redirectTo ?? '/login');
71
84
  }
72
85
  });
@@ -81,18 +94,15 @@
81
94
  {:else if route === '/login' && loginPage}
82
95
  {@render loginPage()}
83
96
  {:else if route === '/login' && authProvider}
84
- <LoginPage {authProvider} {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
97
+ <LoginPage {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
85
98
  {:else if route === '/register' && authProvider?.register}
86
- <RegisterPage {authProvider} {title} />
99
+ <RegisterPage {title} />
87
100
  {:else if route === '/forgot-password' && authProvider?.forgotPassword}
88
- <ForgotPasswordPage {authProvider} {title} />
89
- {:else if route === '/login' || route === '/register' || route === '/forgot-password'}
90
- <div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
91
- <div class="w-full max-w-sm rounded-2xl bg-white dark:bg-gray-900 p-8 shadow-xl text-center">
92
- <h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{title}</h1>
93
- <p class="mt-2 text-sm text-gray-500">Please configure an authProvider.</p>
94
- </div>
95
- </div>
101
+ <ForgotPasswordPage {title} />
102
+ {:else if route === '/update-password' && authProvider?.updatePassword}
103
+ <UpdatePasswordPage {title} />
104
+ {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
105
+ <ConfigErrorScreen title="{title} — Configuration Required" />
96
106
  {:else if isAuthenticated || !authProvider}
97
107
  <Layout {title}>
98
108
  {#if route === '/'}
@@ -100,8 +110,8 @@
100
110
  {@render dashboard()}
101
111
  {:else}
102
112
  <div class="space-y-4">
103
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Welcome to {title}</h1>
104
- <p class="text-gray-500">Select a resource from the sidebar to get started.</p>
113
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('common.welcome', { title })}</h1>
114
+ <p class="text-gray-500">{t('common.dashboardHint')}</p>
105
115
  </div>
106
116
  {/if}
107
117
  {:else if route === '/:resource'}
@@ -124,7 +134,7 @@
124
134
  </Layout>
125
135
  {:else}
126
136
  <div class="flex h-screen items-center justify-center">
127
- <p class="text-gray-500">Redirecting to login...</p>
137
+ <p class="text-gray-500">{t('common.redirecting')}</p>
128
138
  </div>
129
139
  {/if}
130
140
  <Toast />
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { useIsAuthenticated } from '@svadmin/core';
3
+ import { onMount, type Snippet } from 'svelte';
4
+ import { navigate } from '@svadmin/core';
5
+
6
+ let {
7
+ children,
8
+ fallback,
9
+ loading,
10
+ redirectTo = '/login',
11
+ appendCurrentPathToQuery = true,
12
+ } = $props<{
13
+ children?: Snippet;
14
+ fallback?: Snippet;
15
+ loading?: Snippet;
16
+ redirectTo?: string;
17
+ appendCurrentPathToQuery?: boolean;
18
+ }>();
19
+
20
+ const isAuth = useIsAuthenticated();
21
+
22
+ function triggerRedirect() {
23
+ if (fallback) return;
24
+ let url = redirectTo;
25
+ if (appendCurrentPathToQuery && typeof window !== 'undefined') {
26
+ const current = encodeURIComponent(window.location.pathname + window.location.search);
27
+ url += `?to=${current}`;
28
+ }
29
+ navigate(url);
30
+ }
31
+
32
+ onMount(() => {
33
+ if (!isAuth.isLoading && !isAuth.isAuthenticated) {
34
+ triggerRedirect();
35
+ }
36
+ });
37
+
38
+ $effect(() => {
39
+ if (!isAuth.isLoading && !isAuth.isAuthenticated) {
40
+ triggerRedirect();
41
+ }
42
+ });
43
+ </script>
44
+
45
+ {#if isAuth.isLoading}
46
+ {#if loading}
47
+ {@render loading()}
48
+ {/if}
49
+ {:else if isAuth.isAuthenticated}
50
+ {#if children}
51
+ {@render children()}
52
+ {/if}
53
+ {:else}
54
+ {#if fallback}
55
+ {@render fallback()}
56
+ {/if}
57
+ {/if}
@@ -62,8 +62,8 @@
62
62
  }
63
63
  formData = defaults;
64
64
  initialized = true;
65
- } else if (existingQuery && existingQuery?.data) {
66
- formData = { ...existingQuery.data as Record<string, unknown> };
65
+ } else if (existingQuery && existingQuery.query.data?.data) {
66
+ formData = { ...existingQuery.query.data.data as Record<string, unknown> };
67
67
  initialized = true;
68
68
  }
69
69
  });
@@ -125,9 +125,9 @@
125
125
  }
126
126
 
127
127
  if (mode === 'create') {
128
- await createMut.mutateAsync(cleanData);
128
+ await createMut.mutation.mutateAsync({ variables: cleanData });
129
129
  } else if (id != null) {
130
- await updateMut.mutateAsync({ id, variables: cleanData });
130
+ await updateMut.mutation.mutateAsync({ id, variables: cleanData });
131
131
  }
132
132
 
133
133
  isDirty = false;
@@ -150,7 +150,7 @@
150
150
  }
151
151
  }
152
152
 
153
- const isLoading = $derived(mode === 'edit' && existingQuery ? existingQuery?.isLoading : false);
153
+ const isLoading = $derived(mode === 'edit' && existingQuery ? existingQuery.query.isLoading : false);
154
154
 
155
155
  const pageTitle = $derived(
156
156
  mode === 'create'
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { Loader2, Check, X, Cloud } from 'lucide-svelte';
3
+ import { t } from '@svadmin/core/i18n';
4
+
5
+ let {
6
+ status = 'idle',
7
+ error,
8
+ } = $props<{
9
+ /** The current auto-save status */
10
+ status?: 'idle' | 'loading' | 'success' | 'error';
11
+ /** Optional error message */
12
+ error?: string;
13
+ }>();
14
+ </script>
15
+
16
+ <div class="flex items-center gap-2 text-sm text-muted-foreground transition-all duration-300">
17
+ {#if status === 'idle'}
18
+ <Cloud class="h-4 w-4" />
19
+ <span>{t('autoSave.idle')}</span>
20
+ {:else if status === 'loading'}
21
+ <Loader2 class="h-4 w-4 animate-spin text-primary" />
22
+ <span>{t('autoSave.saving')}</span>
23
+ {:else if status === 'success'}
24
+ <Check class="h-4 w-4 text-green-500" />
25
+ <span class="text-green-500">{t('autoSave.saved')}</span>
26
+ {:else if status === 'error'}
27
+ <X class="h-4 w-4 text-destructive" />
28
+ <span class="text-destructive" title={error}>{t('autoSave.error')}</span>
29
+ {/if}
30
+ </div>
@@ -10,10 +10,20 @@
10
10
  import { Checkbox } from './ui/checkbox/index.js';
11
11
  import { Badge } from './ui/badge/index.js';
12
12
  import * as Table from './ui/table/index.js';
13
- import { Plus, Pencil, Trash2, ChevronLeft, ChevronRight, Search, Loader2, Download, Upload } from 'lucide-svelte';
13
+ import { Plus, Pencil, Trash2, ChevronLeft, ChevronRight, Search, Loader2, Download, Upload, ChevronDown, ChevronUp, SlidersHorizontal } from 'lucide-svelte';
14
14
  import ConfirmDialog from './ConfirmDialog.svelte';
15
15
 
16
- let { resourceName } = $props<{ resourceName: string }>();
16
+ import type { Snippet } from 'svelte';
17
+
18
+ let {
19
+ resourceName,
20
+ expandedRowRender,
21
+ selectable = true,
22
+ } = $props<{
23
+ resourceName: string;
24
+ expandedRowRender?: Snippet<[record: Record<string, unknown>]>;
25
+ selectable?: boolean;
26
+ }>();
17
27
 
18
28
  const resource = getResource(resourceName);
19
29
  const primaryKey = $derived(resource.primaryKey ?? 'id');
@@ -33,6 +43,25 @@
33
43
  let filters = $state<Filter[]>([]);
34
44
  let searchText = $state(urlState.search ?? '');
35
45
 
46
+ // Column visibility
47
+ let columnVisibility = $state<Record<string, boolean>>({});
48
+ function initColumnVisibility() {
49
+ const vis: Record<string, boolean> = {};
50
+ for (const f of resource.fields) vis[f.key] = f.showInList !== false;
51
+ return vis;
52
+ }
53
+ columnVisibility = initColumnVisibility();
54
+ const visibleFields = $derived(resource.fields.filter(f => columnVisibility[f.key] !== false && f.showInList !== false));
55
+ let showColumnPicker = $state(false);
56
+
57
+ // Expanded rows
58
+ let expandedRowIds = $state<Set<string | number>>(new Set());
59
+ function toggleExpand(id: string | number) {
60
+ const next = new Set(expandedRowIds);
61
+ if (next.has(id)) next.delete(id); else next.add(id);
62
+ expandedRowIds = next;
63
+ }
64
+
36
65
  // Batch selection
37
66
  let selectedIds = $state<Set<string | number>>(new Set());
38
67
  let selectAll = $state(false);
@@ -54,7 +83,7 @@
54
83
  });
55
84
 
56
85
  const searchableFields = resource.fields.filter(f => f.searchable);
57
- const listFields = resource.fields.filter(f => f.showInList !== false);
86
+ const listFields = $derived(visibleFields);
58
87
 
59
88
  const activeFilters = $derived.by(() => {
60
89
  const result: Filter[] = [...filters];
@@ -64,14 +93,16 @@
64
93
  return result;
65
94
  });
66
95
 
67
- const query = useList({
96
+ const listResult = useList({
68
97
  resource: resourceName,
69
98
  pagination,
70
99
  sorters,
71
100
  filters: activeFilters,
72
101
  });
102
+ const query = listResult.query;
73
103
 
74
- const deleteMutation = useDelete(resourceName);
104
+ const deleteResult = useDelete({ resource: resourceName });
105
+ const deleteMutation = deleteResult.mutation;
75
106
 
76
107
  function toggleSort(field: string) {
77
108
  const existing = sorters.find(s => s.field === field);
@@ -111,7 +142,7 @@
111
142
  function confirmDelete(id: string | number) {
112
143
  confirmMessage = t('common.deleteConfirm');
113
144
  confirmAction = async () => {
114
- await deleteMutation.mutateAsync(id);
145
+ await deleteMutation.mutateAsync({ id, resource: resourceName });
115
146
  confirmOpen = false;
116
147
  };
117
148
  confirmOpen = true;
@@ -121,7 +152,7 @@
121
152
  confirmMessage = t('common.batchDeleteConfirm', { count: selectedIds.size });
122
153
  confirmAction = async () => {
123
154
  for (const id of selectedIds) {
124
- await deleteMutation.mutateAsync(id);
155
+ await deleteMutation.mutateAsync({ id, resource: resourceName });
125
156
  }
126
157
  selectedIds = new Set();
127
158
  selectAll = false;
@@ -194,6 +225,25 @@
194
225
  <Trash2 class="h-4 w-4" data-icon="inline-start" /> {t('common.batchDelete', { count: selectedIds.size })}
195
226
  </Button>
196
227
  {/if}
228
+ <!-- Column Visibility Picker -->
229
+ <div class="relative">
230
+ <Button variant="outline" size="sm" onclick={() => showColumnPicker = !showColumnPicker}>
231
+ <SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns') || 'Columns'}
232
+ </Button>
233
+ {#if showColumnPicker}
234
+ <div class="absolute right-0 top-full mt-1 z-50 w-48 rounded-lg border bg-popover p-2 shadow-lg">
235
+ {#each resource.fields.filter(f => f.showInList !== false) as field}
236
+ <label class="flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-accent cursor-pointer">
237
+ <Checkbox
238
+ checked={columnVisibility[field.key] !== false}
239
+ onCheckedChange={(v) => { columnVisibility = { ...columnVisibility, [field.key]: !!v }; }}
240
+ />
241
+ {field.label}
242
+ </label>
243
+ {/each}
244
+ </div>
245
+ {/if}
246
+ </div>
197
247
  {#if canCreate}
198
248
  <Button onclick={() => navigate(`/${resourceName}/create`)}>
199
249
  <Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
@@ -230,11 +280,14 @@
230
280
  <Table.Root>
231
281
  <Table.Header>
232
282
  <Table.Row class="bg-muted/50 hover:bg-muted/50">
233
- {#if canDelete}
283
+ {#if selectable && canDelete}
234
284
  <Table.Head class="w-10">
235
285
  <Checkbox checked={selectAll} onCheckedChange={() => toggleSelectAll()} />
236
286
  </Table.Head>
237
287
  {/if}
288
+ {#if expandedRowRender}
289
+ <Table.Head class="w-10"></Table.Head>
290
+ {/if}
238
291
  {#each listFields as field}
239
292
  <Table.Head style={field.width ? `width:${field.width}` : undefined}>
240
293
  <button class="flex items-center gap-1 hover:text-foreground" onclick={() => toggleSort(field.key)}>
@@ -250,11 +303,22 @@
250
303
  {#each query.data?.data ?? [] as record}
251
304
  {@const id = record[primaryKey] as string | number}
252
305
  <Table.Row class="transition-colors {selectedIds.has(id) ? 'bg-accent' : ''}">
253
- {#if canDelete}
306
+ {#if selectable && canDelete}
254
307
  <Table.Cell>
255
308
  <Checkbox checked={selectedIds.has(id)} onCheckedChange={() => toggleSelect(id)} />
256
309
  </Table.Cell>
257
310
  {/if}
311
+ {#if expandedRowRender}
312
+ <Table.Cell>
313
+ <button class="p-1 hover:bg-accent rounded" onclick={() => toggleExpand(id)}>
314
+ {#if expandedRowIds.has(id)}
315
+ <ChevronUp class="h-4 w-4" />
316
+ {:else}
317
+ <ChevronDown class="h-4 w-4" />
318
+ {/if}
319
+ </button>
320
+ </Table.Cell>
321
+ {/if}
258
322
  {#each listFields as field}
259
323
  <Table.Cell>
260
324
  {#if field.type === 'boolean'}
@@ -299,9 +363,16 @@
299
363
  </div>
300
364
  </Table.Cell>
301
365
  </Table.Row>
366
+ {#if expandedRowRender && expandedRowIds.has(id)}
367
+ <Table.Row class="bg-muted/30">
368
+ <Table.Cell colspan={listFields.length + (selectable && canDelete ? 1 : 0) + (expandedRowRender ? 1 : 0) + 1}>
369
+ {@render expandedRowRender(record)}
370
+ </Table.Cell>
371
+ </Table.Row>
372
+ {/if}
302
373
  {:else}
303
374
  <Table.Row>
304
- <Table.Cell colspan={listFields.length + (canDelete ? 2 : 1)} class="h-24 text-center text-muted-foreground">
375
+ <Table.Cell colspan={listFields.length + (selectable && canDelete ? 2 : 1) + (expandedRowRender ? 1 : 0)} class="h-24 text-center text-muted-foreground">
305
376
  {t('common.noData')}
306
377
  </Table.Cell>
307
378
  </Table.Row>
@@ -29,6 +29,8 @@
29
29
  result.push({ label: t('common.create'), href: `#/${res.name}/create` });
30
30
  } else if (segments[1] === 'edit' && segments[2]) {
31
31
  result.push({ label: `${t('common.edit')} #${segments[2]}`, href: `#/${res.name}/edit/${segments[2]}` });
32
+ } else if (segments[1] === 'show' && segments[2]) {
33
+ result.push({ label: `${t('common.detail')} #${segments[2]}`, href: `#/${res.name}/show/${segments[2]}` });
32
34
  }
33
35
  }
34
36
  return result;
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { useIsAuthenticated } from '@svadmin/core';
3
+ import { navigate } from '@svadmin/core/router';
4
+ import NavigateToResource from './NavigateToResource.svelte';
5
+
6
+ const auth = useIsAuthenticated();
7
+
8
+ $effect(() => {
9
+ if (!auth.isLoading && !auth.isAuthenticated) {
10
+ navigate('/login');
11
+ }
12
+ });
13
+ </script>
14
+
15
+ {#if auth.isLoading}
16
+ <div class="flex h-screen w-screen items-center justify-center">
17
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"></div>
18
+ </div>
19
+ {:else if auth.isAuthenticated}
20
+ <NavigateToResource />
21
+ {/if}