@svadmin/ui 0.34.0 → 0.34.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.
@@ -7,7 +7,7 @@
7
7
  import { t } from '@svadmin/core/i18n';
8
8
  import { navigate } from '@svadmin/core/router';
9
9
  import { currentPath } from '@svadmin/core/router';
10
- import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
10
+ import { QueryClient, QueryClientProvider, type DefaultOptions } from '@tanstack/svelte-query';
11
11
  import { setComponentRegistry, getComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
12
12
  import Layout from './Layout.svelte';
13
13
  import AutoTable from './AutoTable.svelte';
@@ -51,6 +51,10 @@
51
51
  siteUrl?: string;
52
52
  /** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
53
53
  routeMode?: 'hash' | 'path' | 'auto';
54
+ /** Optional TanStack Query client override. */
55
+ queryClient?: QueryClient;
56
+ /** Optional TanStack Query default options override when QueryClient is not supplied. */
57
+ queryClientDefaultOptions?: DefaultOptions;
54
58
  }
55
59
 
56
60
  let {
@@ -69,6 +73,8 @@
69
73
  menu,
70
74
  siteUrl,
71
75
  routeMode,
76
+ queryClient: providedQueryClient,
77
+ queryClientDefaultOptions,
72
78
  }: Props = $props();
73
79
 
74
80
  // Default component registry
@@ -113,11 +119,12 @@
113
119
  if (defaultTheme) setTheme(defaultTheme);
114
120
  });
115
121
 
116
- const queryClient = new QueryClient({
122
+ const queryClient = $derived(providedQueryClient ?? new QueryClient({
117
123
  defaultOptions: {
118
124
  queries: { staleTime: 30_000, retry: 1 },
125
+ ...queryClientDefaultOptions,
119
126
  },
120
- });
127
+ }));
121
128
 
122
129
  // Initialize router with provider synchronously
123
130
  initRouter(resolvedRouter);
@@ -1,6 +1,7 @@
1
1
  import '../app.css';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
4
+ import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
4
5
  import { type ComponentRegistry } from '../component-registry.svelte.js';
5
6
  interface Props {
6
7
  dataProvider: DataProvider;
@@ -23,6 +24,10 @@ interface Props {
23
24
  siteUrl?: string;
24
25
  /** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
25
26
  routeMode?: 'hash' | 'path' | 'auto';
27
+ /** Optional TanStack Query client override. */
28
+ queryClient?: QueryClient;
29
+ /** Optional TanStack Query default options override when QueryClient is not supplied. */
30
+ queryClientDefaultOptions?: DefaultOptions;
26
31
  }
27
32
  declare const AdminApp: import("svelte").Component<Props, {}, "">;
28
33
  type AdminApp = ReturnType<typeof AdminApp>;
@@ -9,6 +9,17 @@
9
9
  import TaskProgressBar from './TaskProgressBar.svelte';
10
10
  import RetryTaskButton from './RetryTaskButton.svelte';
11
11
  import CancelTaskButton from './CancelTaskButton.svelte';
12
+ import {
13
+ canCancelTask,
14
+ canRetryTask,
15
+ resolveTaskCreatedAt,
16
+ resolveTaskError,
17
+ resolveTaskMessage,
18
+ resolveTaskProgress,
19
+ resolveTaskResult,
20
+ resolveTaskTitle,
21
+ resolveTaskUpdatedAt,
22
+ } from './task-utils.js';
12
23
 
13
24
  let {
14
25
  task,
@@ -50,7 +61,9 @@
50
61
 
51
62
  function formatDate(value: unknown) {
52
63
  if (!value) return '—';
53
- return new Date(value as string | Date).toLocaleString();
64
+ const date = new Date(value as string | Date);
65
+ if (Number.isNaN(date.getTime())) return String(value);
66
+ return date.toLocaleString();
54
67
  }
55
68
 
56
69
  function toPrettyJson(value: unknown) {
@@ -67,7 +80,7 @@
67
80
  <Card.Title class="text-base">{resolvedTitle}</Card.Title>
68
81
  <Card.Description>
69
82
  {#if resolvedTask}
70
- {String(resolvedTask.title ?? resolvedTask.name ?? resolvedTask.id)}
83
+ {resolveTaskTitle(resolvedTask)}
71
84
  {:else}
72
85
  {t('task.detailsSummary')}
73
86
  {/if}
@@ -85,46 +98,53 @@
85
98
  <Badge variant="outline" class="font-mono text-xs">{resolvedTask.id}</Badge>
86
99
  </div>
87
100
 
88
- {#if typeof resolvedTask.progress === 'number'}
89
- <TaskProgressBar value={Number(resolvedTask.progress)} />
101
+ {#if typeof resolveTaskProgress(resolvedTask) === 'number'}
102
+ <TaskProgressBar value={resolveTaskProgress(resolvedTask)} />
90
103
  {/if}
91
104
 
92
105
  <div class="grid gap-4 md:grid-cols-2">
93
106
  <div class="space-y-1">
94
107
  <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.createdLabel')}</div>
95
- <div class="text-sm">{formatDate(resolvedTask.createdAt)}</div>
108
+ <div class="text-sm">{formatDate(resolveTaskCreatedAt(resolvedTask))}</div>
96
109
  </div>
97
110
  <div class="space-y-1">
98
111
  <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.updatedLabel')}</div>
99
- <div class="text-sm">{formatDate(resolvedTask.updatedAt)}</div>
112
+ <div class="text-sm">{formatDate(resolveTaskUpdatedAt(resolvedTask))}</div>
100
113
  </div>
101
114
  </div>
102
115
 
103
- {#if resolvedTask.message}
116
+ {#if resolveTaskMessage(resolvedTask)}
104
117
  <div class="space-y-1">
105
118
  <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.messageLabel')}</div>
106
- <div class="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">{String(resolvedTask.message)}</div>
119
+ <div class="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">{resolveTaskMessage(resolvedTask)}</div>
107
120
  </div>
108
121
  {/if}
109
122
 
110
- {#if resolvedTask.error}
123
+ {#if resolveTaskError(resolvedTask)}
111
124
  <div class="space-y-1">
112
125
  <div class="text-xs uppercase tracking-wide text-destructive">{t('task.errorLabel')}</div>
113
- <pre class="overflow-auto rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">{toPrettyJson(resolvedTask.error)}</pre>
126
+ <pre class="overflow-auto rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">{toPrettyJson(resolveTaskError(resolvedTask))}</pre>
127
+ </div>
128
+ {/if}
129
+
130
+ {#if resolveTaskResult(resolvedTask) !== undefined}
131
+ <div class="space-y-1">
132
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.resultLabel')}</div>
133
+ <pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolveTaskResult(resolvedTask))}</pre>
114
134
  </div>
115
135
  {/if}
116
136
 
117
137
  <div class="space-y-1">
118
138
  <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.payloadLabel')}</div>
119
- <pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolvedTask)}</pre>
139
+ <pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolvedTask.payload ?? resolvedTask)}</pre>
120
140
  </div>
121
141
 
122
142
  {#if taskProvider?.retry || taskProvider?.cancel}
123
143
  <div class="flex flex-wrap gap-2 pt-2">
124
- {#if taskProvider?.retry && ['failed', 'error'].includes(String(resolvedTask.status).toLowerCase())}
144
+ {#if taskProvider?.retry && canRetryTask(resolvedTask)}
125
145
  <RetryTaskButton taskId={resolvedTask.id} {taskProvider} />
126
146
  {/if}
127
- {#if taskProvider?.cancel && ['pending', 'queued', 'running', 'processing'].includes(String(resolvedTask.status).toLowerCase())}
147
+ {#if taskProvider?.cancel && canCancelTask(resolvedTask)}
128
148
  <CancelTaskButton taskId={resolvedTask.id} {taskProvider} />
129
149
  {/if}
130
150
  </div>
@@ -10,6 +10,14 @@
10
10
  import TaskProgressBar from './TaskProgressBar.svelte';
11
11
  import RetryTaskButton from './RetryTaskButton.svelte';
12
12
  import CancelTaskButton from './CancelTaskButton.svelte';
13
+ import {
14
+ canCancelTask,
15
+ canRetryTask,
16
+ resolveTaskMessage,
17
+ resolveTaskProgress,
18
+ resolveTaskTitle,
19
+ resolveTaskUpdatedAt,
20
+ } from './task-utils.js';
13
21
 
14
22
  let {
15
23
  tasks,
@@ -65,7 +73,9 @@
65
73
 
66
74
  function formatDate(value: unknown) {
67
75
  if (!value) return '—';
68
- return new Date(value as string | Date).toLocaleString();
76
+ const date = new Date(value as string | Date);
77
+ if (Number.isNaN(date.getTime())) return String(value);
78
+ return date.toLocaleString();
69
79
  }
70
80
  </script>
71
81
 
@@ -104,12 +114,12 @@
104
114
  <Table.Row class="cursor-pointer hover:bg-muted/40" onclick={() => onSelect?.(task)}>
105
115
  <Table.Cell class="min-w-[240px]">
106
116
  <div class="space-y-1">
107
- <div class="font-medium text-foreground">{String(task.title ?? task.name ?? task.id)}</div>
108
- {#if task.message}
109
- <div class="text-xs text-muted-foreground">{String(task.message)}</div>
117
+ <div class="font-medium text-foreground">{resolveTaskTitle(task)}</div>
118
+ {#if resolveTaskMessage(task)}
119
+ <div class="text-xs text-muted-foreground">{resolveTaskMessage(task)}</div>
110
120
  {/if}
111
- {#if showProgress && typeof task.progress === 'number' && String(task.status).toLowerCase() === 'running'}
112
- <TaskProgressBar value={Number(task.progress)} />
121
+ {#if showProgress && typeof resolveTaskProgress(task) === 'number'}
122
+ <TaskProgressBar value={resolveTaskProgress(task)} />
113
123
  {/if}
114
124
  </div>
115
125
  </Table.Cell>
@@ -117,15 +127,15 @@
117
127
  <TaskStatusBadge status={String(task.status ?? 'pending')} />
118
128
  </Table.Cell>
119
129
  <Table.Cell class="text-xs text-muted-foreground">
120
- {formatDate(task.updatedAt ?? task.createdAt)}
130
+ {formatDate(resolveTaskUpdatedAt(task))}
121
131
  </Table.Cell>
122
132
  {#if showActions}
123
133
  <Table.Cell class="text-right">
124
134
  <div class="flex justify-end gap-2">
125
- {#if taskProvider?.retry && ['failed', 'error'].includes(String(task.status).toLowerCase())}
135
+ {#if taskProvider?.retry && canRetryTask(task)}
126
136
  <RetryTaskButton taskId={task.id} {taskProvider} />
127
137
  {/if}
128
- {#if taskProvider?.cancel && ['pending', 'queued', 'running', 'processing'].includes(String(task.status).toLowerCase())}
138
+ {#if taskProvider?.cancel && canCancelTask(task)}
129
139
  <CancelTaskButton taskId={task.id} {taskProvider} />
130
140
  {/if}
131
141
  <Button variant="ghost" size="sm" onclick={(e) => { e.stopPropagation(); onSelect?.(task); }}>
@@ -14,6 +14,12 @@
14
14
  import * as Card from './ui/card/index.js';
15
15
  import TaskList from './TaskList.svelte';
16
16
  import TaskDetails from './TaskDetails.svelte';
17
+ import {
18
+ isTaskActive,
19
+ resolveTaskCreatedAt,
20
+ resolveTaskMessage,
21
+ resolveTaskTitle,
22
+ } from './task-utils.js';
17
23
 
18
24
  type AutoRefreshValue = '0' | '5000' | '15000' | '30000';
19
25
  type TaskTab = 'tasks' | 'dlq';
@@ -41,7 +47,9 @@
41
47
  let submitError = $state<string | null>(null);
42
48
  let submitOpen = $state(false);
43
49
 
44
- const refreshInterval = $derived(autoRefresh === '0' ? false : Number(autoRefresh));
50
+ const refreshInterval = $derived.by<number | false>(() =>
51
+ autoRefresh === '0' ? false : Number(autoRefresh),
52
+ );
45
53
  const resolvedTitle = $derived(title ?? t('task.drawerTitle'));
46
54
 
47
55
  const taskQuery = useTaskList({
@@ -88,10 +96,9 @@
88
96
  if (!query) return true;
89
97
  const haystack = [
90
98
  task.id,
91
- task.title,
92
- task.name,
99
+ resolveTaskTitle(task),
93
100
  task.status,
94
- task.message,
101
+ resolveTaskMessage(task),
95
102
  ]
96
103
  .filter(Boolean)
97
104
  .join(' ')
@@ -110,7 +117,7 @@
110
117
  });
111
118
 
112
119
  const runningCount = $derived(
113
- allTasks.filter((task) => ['pending', 'queued', 'running', 'processing'].includes(String(task.status ?? '').toLowerCase())).length,
120
+ allTasks.filter(isTaskActive).length,
114
121
  );
115
122
 
116
123
  $effect(() => {
@@ -142,7 +149,9 @@
142
149
 
143
150
  function formatTime(value: unknown) {
144
151
  if (!value) return '—';
145
- return new Date(value as string | Date).toLocaleString();
152
+ const date = new Date(value as string | Date);
153
+ if (Number.isNaN(date.getTime())) return String(value);
154
+ return date.toLocaleString();
146
155
  }
147
156
 
148
157
  function resetSubmitForm() {
@@ -223,8 +232,8 @@
223
232
  <ListTodo class="h-5 w-5 text-muted-foreground" />
224
233
  <h2 class="text-sm font-semibold">{resolvedTitle}</h2>
225
234
  <Badge variant="secondary" class="text-xs">{t('task.activeCount', { count: runningCount })}</Badge>
226
- {#if allTasks[0]?.createdAt}
227
- <span class="text-xs text-muted-foreground">{t('task.lastQueuedAt', { time: formatTime(allTasks[0].createdAt) })}</span>
235
+ {#if allTasks[0] && resolveTaskCreatedAt(allTasks[0])}
236
+ <span class="text-xs text-muted-foreground">{t('task.lastQueuedAt', { time: formatTime(resolveTaskCreatedAt(allTasks[0])) })}</span>
228
237
  {/if}
229
238
  </div>
230
239
  </div>
@@ -4,31 +4,31 @@ import TaskQueueDrawer from './TaskQueueDrawer.svelte';
4
4
  const taskFixtures = [
5
5
  {
6
6
  id: 'task-1',
7
- title: 'Generate poster',
8
- status: 'running',
7
+ name: 'Generate poster',
8
+ status: 'processing',
9
9
  message: 'Rendering hero artwork',
10
- createdAt: '2026-04-19T10:00:00.000Z',
11
- updatedAt: '2026-04-19T10:05:00.000Z',
10
+ created_at: '2026-04-19T10:00:00.000Z',
11
+ updated_at: '2026-04-19T10:05:00.000Z',
12
12
  progress: 42,
13
13
  },
14
14
  {
15
15
  id: 'task-2',
16
- title: 'Send email batch',
16
+ name: 'Send email batch',
17
17
  status: 'queued',
18
18
  message: 'Waiting for worker',
19
- createdAt: '2026-04-19T10:10:00.000Z',
20
- updatedAt: '2026-04-19T10:12:00.000Z',
19
+ created_at: '2026-04-19T10:10:00.000Z',
20
+ updated_at: '2026-04-19T10:12:00.000Z',
21
21
  },
22
22
  ];
23
23
  const dlqFixtures = [
24
24
  {
25
25
  id: 'task-dlq-1',
26
- title: 'Nightly sync',
27
- status: 'failed',
28
- message: 'Remote API timeout',
29
- createdAt: '2026-04-18T23:00:00.000Z',
30
- updatedAt: '2026-04-18T23:05:00.000Z',
31
- error: { message: 'Remote API timeout' },
26
+ name: 'Nightly sync',
27
+ status: 'dead_lettered',
28
+ error_message: 'Remote API timeout',
29
+ created_at: '2026-04-18T23:00:00.000Z',
30
+ updated_at: '2026-04-18T23:05:00.000Z',
31
+ result_data: { reason: 'Remote API timeout' },
32
32
  },
33
33
  ];
34
34
  const mockTaskProvider = {
@@ -36,10 +36,10 @@ const mockTaskProvider = {
36
36
  id: 'submitted-image.generate',
37
37
  wait: async () => ({
38
38
  id: 'submitted-image.generate',
39
- title: 'image.generate',
39
+ name: 'image.generate',
40
40
  status: 'queued',
41
- createdAt: '2026-04-19T11:00:00.000Z',
42
- updatedAt: '2026-04-19T11:00:00.000Z',
41
+ created_at: '2026-04-19T11:00:00.000Z',
42
+ updated_at: '2026-04-19T11:00:00.000Z',
43
43
  }),
44
44
  })),
45
45
  get: vi.fn(async (taskId) => {
@@ -56,7 +56,7 @@ const mutateAsync = vi.fn(async (params) => ({
56
56
  id: `submitted-${params.taskName}`,
57
57
  wait: async () => ({
58
58
  id: `submitted-${params.taskName}`,
59
- title: params.taskName,
59
+ name: params.taskName,
60
60
  status: 'queued',
61
61
  }),
62
62
  }));
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { t } from '@svadmin/core/i18n';
3
- import { Clock3, Loader2, CheckCircle2, AlertTriangle, CircleDashed } from '@lucide/svelte';
3
+ import { Ban, Clock3, Loader2, CheckCircle2, AlertTriangle, CircleDashed } from '@lucide/svelte';
4
4
  import { Badge } from './ui/badge/index.js';
5
+ import { normalizeTaskStatus } from './task-utils.js';
5
6
 
6
7
  let {
7
8
  status = 'pending',
@@ -14,6 +15,7 @@
14
15
  }>();
15
16
 
16
17
  const normalizedStatus = $derived(String(status || 'pending').toLowerCase());
18
+ const lifecycle = $derived(normalizeTaskStatus(normalizedStatus));
17
19
 
18
20
  function translateStatus(value: string) {
19
21
  const key = `task.status.${value}`;
@@ -23,8 +25,7 @@
23
25
  }
24
26
 
25
27
  const config = $derived.by(() => {
26
- switch (normalizedStatus) {
27
- case 'running':
28
+ switch (lifecycle) {
28
29
  case 'processing':
29
30
  return {
30
31
  label: translateStatus(normalizedStatus),
@@ -33,8 +34,6 @@
33
34
  iconClass: 'animate-spin',
34
35
  };
35
36
  case 'completed':
36
- case 'success':
37
- case 'done':
38
37
  return {
39
38
  label: translateStatus(normalizedStatus),
40
39
  variant: 'secondary' as const,
@@ -42,21 +41,26 @@
42
41
  iconClass: 'text-green-600 dark:text-green-400',
43
42
  };
44
43
  case 'failed':
45
- case 'error':
46
44
  return {
47
45
  label: translateStatus(normalizedStatus),
48
46
  variant: 'destructive' as const,
49
47
  icon: AlertTriangle,
50
48
  iconClass: '',
51
49
  };
50
+ case 'cancelled':
51
+ return {
52
+ label: translateStatus(normalizedStatus),
53
+ variant: 'outline' as const,
54
+ icon: Ban,
55
+ iconClass: 'text-muted-foreground',
56
+ };
52
57
  case 'queued':
53
58
  return {
54
- label: t('task.status.queued'),
59
+ label: translateStatus(normalizedStatus),
55
60
  variant: 'outline' as const,
56
61
  icon: CircleDashed,
57
62
  iconClass: '',
58
63
  };
59
- case 'pending':
60
64
  default:
61
65
  return {
62
66
  label: translateStatus(normalizedStatus),
@@ -0,0 +1,14 @@
1
+ import type { TaskRecord } from '@svadmin/core';
2
+ export type TaskLifecycle = 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'unknown';
3
+ export declare function normalizeTaskStatus(status: unknown): TaskLifecycle;
4
+ export declare function isTaskActive(task: TaskRecord): boolean;
5
+ export declare function canCancelTask(task: TaskRecord): boolean;
6
+ export declare function canRetryTask(task: TaskRecord): boolean;
7
+ export declare function resolveTaskTitle(task: TaskRecord): string;
8
+ export declare function resolveTaskMessage(task: TaskRecord): string;
9
+ export declare function resolveTaskUpdatedAt(task: TaskRecord): unknown;
10
+ export declare function resolveTaskCreatedAt(task: TaskRecord): unknown;
11
+ export declare function resolveTaskQueueName(task: TaskRecord): string;
12
+ export declare function resolveTaskResult(task: TaskRecord): unknown;
13
+ export declare function resolveTaskError(task: TaskRecord): unknown;
14
+ export declare function resolveTaskProgress(task: TaskRecord): number | undefined;
@@ -0,0 +1,69 @@
1
+ const STATUS_ALIASES = {
2
+ pending: 'queued',
3
+ queued: 'queued',
4
+ retry_scheduled: 'queued',
5
+ scheduled: 'queued',
6
+ leased: 'processing',
7
+ running: 'processing',
8
+ processing: 'processing',
9
+ active: 'processing',
10
+ completed: 'completed',
11
+ complete: 'completed',
12
+ success: 'completed',
13
+ succeeded: 'completed',
14
+ done: 'completed',
15
+ failed: 'failed',
16
+ error: 'failed',
17
+ dead_lettered: 'failed',
18
+ cancelled: 'cancelled',
19
+ canceled: 'cancelled',
20
+ aborted: 'cancelled',
21
+ };
22
+ export function normalizeTaskStatus(status) {
23
+ const value = String(status ?? '').trim().toLowerCase();
24
+ return STATUS_ALIASES[value] ?? 'unknown';
25
+ }
26
+ export function isTaskActive(task) {
27
+ const lifecycle = normalizeTaskStatus(task.status);
28
+ return lifecycle === 'queued' || lifecycle === 'processing';
29
+ }
30
+ export function canCancelTask(task) {
31
+ return isTaskActive(task);
32
+ }
33
+ export function canRetryTask(task) {
34
+ return normalizeTaskStatus(task.status) === 'failed';
35
+ }
36
+ export function resolveTaskTitle(task) {
37
+ return String(task.title ?? task.name ?? task.id);
38
+ }
39
+ export function resolveTaskMessage(task) {
40
+ return String(task.message ?? task.errorMessage ?? task.error_message ?? '');
41
+ }
42
+ export function resolveTaskUpdatedAt(task) {
43
+ return (task.updatedAt ??
44
+ task.updated_at ??
45
+ task.finishedAt ??
46
+ task.finished_at ??
47
+ task.startedAt ??
48
+ task.started_at ??
49
+ task.createdAt ??
50
+ task.created_at);
51
+ }
52
+ export function resolveTaskCreatedAt(task) {
53
+ return task.createdAt ?? task.created_at;
54
+ }
55
+ export function resolveTaskQueueName(task) {
56
+ return String(task.queueName ?? task.queue_name ?? '');
57
+ }
58
+ export function resolveTaskResult(task) {
59
+ return task.result ?? task.result_data;
60
+ }
61
+ export function resolveTaskError(task) {
62
+ return task.error ?? task.errorMessage ?? task.error_message;
63
+ }
64
+ export function resolveTaskProgress(task) {
65
+ if (typeof task.progress !== 'number' || !Number.isFinite(task.progress)) {
66
+ return normalizeTaskStatus(task.status) === 'completed' ? 100 : undefined;
67
+ }
68
+ return Math.max(0, Math.min(100, Math.round(task.progress)));
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -26,7 +26,7 @@
26
26
  "./components/*": "./dist/components/*"
27
27
  },
28
28
  "peerDependencies": {
29
- "@svadmin/core": "^0.25.0",
29
+ "@svadmin/core": "^0.25.1",
30
30
  "@tanstack/svelte-query": "^6.0.0",
31
31
  "highlight.js": "^11.11.1",
32
32
  "isomorphic-dompurify": "^3.7.1",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@lucide/svelte": "^1.7.0",
65
- "@tanstack/svelte-table": "^9.0.0-alpha.32",
65
+ "@tanstack/svelte-table": "9.0.0-alpha.32",
66
66
  "bits-ui": "^2.17.1",
67
67
  "clsx": "^2.1.1",
68
68
  "cmdk-sv": "^0.0.19",