@svadmin/ui 0.49.0 → 0.49.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/dist/components/AdminApp.svelte +40 -2
- package/dist/components/AutoTable.svelte +3 -5
- package/dist/components/DetailDrawer.svelte +14 -3
- package/dist/components/DetailDrawer.svelte.d.ts +3 -0
- package/dist/components/Layout.svelte +1 -0
- package/dist/components/ProfilePage.svelte +1 -1
- package/dist/components/RowActions.svelte +76 -3
- package/dist/components/RowActions.svelte.d.ts +1 -1
- package/dist/components/SettingsPage.svelte +1 -1
- package/dist/components/Sidebar.svelte +34 -23
- package/dist/components/content/WorkspaceStageStepper.svelte +13 -1
- package/dist/components/content/WorkspaceStageStepper.svelte.d.ts +2 -0
- package/dist/components/ui/sheet/sheet.svelte +3 -1
- package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
- package/dist/components/ui/table/index.d.ts +1 -0
- package/dist/components/ui/table/table-cell.svelte +8 -1
- package/dist/components/ui/table/table-head.svelte +8 -1
- package/dist/components/ui/table/table.svelte +12 -2
- package/dist/components/ui/table/table.svelte.d.ts +5 -1
- package/dist/router-state.svelte.js +2 -0
- package/package.json +2 -2
|
@@ -351,6 +351,44 @@
|
|
|
351
351
|
const renderedParams = $derived(params);
|
|
352
352
|
const renderedHasRouteResource = $derived(!renderedParams.resource || resourceNames.has(renderedParams.resource));
|
|
353
353
|
const renderedResourcePages = $derived(renderedParams.resource ? resourcePages?.[renderedParams.resource] : undefined);
|
|
354
|
+
const documentTitle = $derived.by(() => {
|
|
355
|
+
const resource = renderedParams.resource
|
|
356
|
+
? resources.find((candidate) => candidate.name === renderedParams.resource)
|
|
357
|
+
: undefined;
|
|
358
|
+
let pageLabel = '';
|
|
359
|
+
|
|
360
|
+
if (resource) {
|
|
361
|
+
pageLabel = resource.label || resource.name;
|
|
362
|
+
} else if (renderedRoute.startsWith('/settings') || renderedRoute === '/account/:tab') {
|
|
363
|
+
const tab = renderedParams.tab === 'api-keys'
|
|
364
|
+
? 'api'
|
|
365
|
+
: renderedParams.tab === 'audit-logs'
|
|
366
|
+
? 'audit'
|
|
367
|
+
: renderedParams.tab ?? 'profile';
|
|
368
|
+
const labels: Record<string, string> = {
|
|
369
|
+
profile: translation.t('settings.profile'),
|
|
370
|
+
appearance: translation.t('settings.appearance'),
|
|
371
|
+
notifications: translation.t('settings.notifications'),
|
|
372
|
+
security: translation.t('settings.security'),
|
|
373
|
+
roles: translation.t('settings.rolesAndPermissions'),
|
|
374
|
+
integrations: translation.t('settings.integrations'),
|
|
375
|
+
api: translation.t('settings.api'),
|
|
376
|
+
audit: translation.t('settings.auditLogs'),
|
|
377
|
+
about: translation.t('settings.about'),
|
|
378
|
+
};
|
|
379
|
+
pageLabel = labels[tab] ?? translation.t('settings.title');
|
|
380
|
+
} else if (renderedRoute === '/login') {
|
|
381
|
+
pageLabel = translation.t('auth.login');
|
|
382
|
+
} else if (renderedRoute === '/' || renderedRoute === '/inventory-dashboard') {
|
|
383
|
+
pageLabel = translation.t('common.dashboard');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return pageLabel && pageLabel !== title ? `${pageLabel} - ${title}` : title;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
$effect(() => {
|
|
390
|
+
if (typeof document !== 'undefined') document.title = documentTitle;
|
|
391
|
+
});
|
|
354
392
|
|
|
355
393
|
async function navigateWithinApp(path: string, options?: { replaceState?: boolean }) {
|
|
356
394
|
await adminContext.navigate(path, options);
|
|
@@ -578,12 +616,12 @@
|
|
|
578
616
|
{@const Comp = renderedResourcePages?.create ?? mergedComponents.AutoForm}
|
|
579
617
|
<Comp resourceName={renderedParams.resource} mode="create" />
|
|
580
618
|
{/key}
|
|
581
|
-
{:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource}
|
|
619
|
+
{:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:resource/:id/edit' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource}
|
|
582
620
|
{#key `${renderedParams.resource}-${renderedParams.id}`}
|
|
583
621
|
{@const Comp = renderedResourcePages?.edit ?? mergedComponents.AutoForm}
|
|
584
622
|
<Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
|
|
585
623
|
{/key}
|
|
586
|
-
{:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource}
|
|
624
|
+
{:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:resource/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource}
|
|
587
625
|
{#key `${renderedParams.resource}-${renderedParams.id}`}
|
|
588
626
|
{@const Comp = renderedResourcePages?.show ?? mergedComponents.ShowPage}
|
|
589
627
|
<Comp resourceName={renderedParams.resource} id={renderedParams.id} />
|
|
@@ -111,8 +111,6 @@
|
|
|
111
111
|
sorters: externalSorters,
|
|
112
112
|
}: Props = $props();
|
|
113
113
|
const adminContext = captureAdminContext();
|
|
114
|
-
const headerDensityClass = $derived(density === 'compact' ? 'h-8 py-1.5 text-[0.6875rem]' : 'h-9 py-2.5 text-xs');
|
|
115
|
-
const cellDensityClass = $derived(density === 'compact' ? 'h-8 px-2 py-1 text-xs' : 'h-10 p-2 text-sm');
|
|
116
114
|
|
|
117
115
|
const resource = $derived(getResource(resourceName));
|
|
118
116
|
const primaryKey = $derived(resource.primaryKey ?? 'id');
|
|
@@ -710,7 +708,7 @@
|
|
|
710
708
|
<div in:fade={{ duration: 150 }}>
|
|
711
709
|
<!-- Desktop Table (hidden on mobile) -->
|
|
712
710
|
<div class="hidden md:block">
|
|
713
|
-
<Table.Root>
|
|
711
|
+
<Table.Root {density}>
|
|
714
712
|
<Table.Header>
|
|
715
713
|
{#each tableView.headerGroups as headerGroup, _i (_i)}
|
|
716
714
|
{@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, BaseRecord, unknown>) => isColumnVisible(header.column.id))}
|
|
@@ -723,7 +721,7 @@
|
|
|
723
721
|
{@const header = col.header as typeof headerGroup.headers[0]}
|
|
724
722
|
<Table.Head
|
|
725
723
|
{...dragProps}
|
|
726
|
-
class={cn('border-b border-border bg-muted/25 font-medium tracking-normal text-muted-foreground hover:bg-muted/40',
|
|
724
|
+
class={cn('border-b border-border bg-muted/25 font-medium tracking-normal text-muted-foreground hover:bg-muted/40', dragProps.class)}
|
|
727
725
|
style={header_getSize(header) != null && header_getSize(header) !== 150 ? `width:${header_getSize(header)}px` : undefined}
|
|
728
726
|
>
|
|
729
727
|
{#if header.id === '_select'}
|
|
@@ -768,7 +766,7 @@
|
|
|
768
766
|
{#snippet child({ props })}
|
|
769
767
|
<Table.Row {...props} class="transition-all duration-300 border-b border-border/10 {row_getIsSelected(row) ? 'bg-primary/5' : 'hover:bg-muted/20'}">
|
|
770
768
|
{#each visibleCells as cell, _i (_i)}
|
|
771
|
-
<Table.Cell
|
|
769
|
+
<Table.Cell>
|
|
772
770
|
{#if cell.column.id === '_select'}
|
|
773
771
|
<Checkbox
|
|
774
772
|
checked={row_getIsSelected(row)}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
8
8
|
open?: boolean;
|
|
9
9
|
title?: string;
|
|
10
|
+
titleId?: string;
|
|
10
11
|
description?: string;
|
|
12
|
+
descriptionId?: string;
|
|
13
|
+
closeLabel?: string;
|
|
11
14
|
side?: 'left' | 'right';
|
|
12
15
|
width?: string;
|
|
13
16
|
onClose?: () => void;
|
|
@@ -24,6 +27,11 @@
|
|
|
24
27
|
ref = $bindable(null),
|
|
25
28
|
class: className,
|
|
26
29
|
'aria-label': ariaLabel,
|
|
30
|
+
'aria-labelledby': ariaLabelledby,
|
|
31
|
+
'aria-describedby': ariaDescribedby,
|
|
32
|
+
titleId,
|
|
33
|
+
descriptionId,
|
|
34
|
+
closeLabel = 'Close',
|
|
27
35
|
onClose,
|
|
28
36
|
children,
|
|
29
37
|
footer,
|
|
@@ -41,20 +49,23 @@
|
|
|
41
49
|
bind:open
|
|
42
50
|
{side}
|
|
43
51
|
onClose={handleClose}
|
|
52
|
+
{closeLabel}
|
|
44
53
|
class={cn('overflow-y-auto', width, className)}
|
|
45
54
|
role="dialog"
|
|
46
55
|
aria-modal="true"
|
|
47
|
-
aria-label={ariaLabel || title || 'Details'}
|
|
56
|
+
aria-label={(ariaLabelledby || titleId) ? ariaLabel : (ariaLabel || title || 'Details')}
|
|
57
|
+
aria-labelledby={ariaLabelledby || (titleId ? titleId : undefined)}
|
|
58
|
+
aria-describedby={ariaDescribedby || (descriptionId ? descriptionId : undefined)}
|
|
48
59
|
{...restProps}
|
|
49
60
|
>
|
|
50
61
|
<Sheet.Content class="flex h-full flex-col">
|
|
51
62
|
{#if title || description}
|
|
52
63
|
<Sheet.Header>
|
|
53
64
|
{#if title}
|
|
54
|
-
<Sheet.Title>{title}</Sheet.Title>
|
|
65
|
+
<Sheet.Title id={titleId}>{title}</Sheet.Title>
|
|
55
66
|
{/if}
|
|
56
67
|
{#if description}
|
|
57
|
-
<Sheet.Description>{description}</Sheet.Description>
|
|
68
|
+
<Sheet.Description id={descriptionId}>{description}</Sheet.Description>
|
|
58
69
|
{/if}
|
|
59
70
|
</Sheet.Header>
|
|
60
71
|
{/if}
|
|
@@ -4,7 +4,10 @@ import { type WithElementRef } from '../utils.js';
|
|
|
4
4
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
5
5
|
open?: boolean;
|
|
6
6
|
title?: string;
|
|
7
|
+
titleId?: string;
|
|
7
8
|
description?: string;
|
|
9
|
+
descriptionId?: string;
|
|
10
|
+
closeLabel?: string;
|
|
8
11
|
side?: 'left' | 'right';
|
|
9
12
|
width?: string;
|
|
10
13
|
onClose?: () => void;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const identity = useGetIdentity();
|
|
22
22
|
const notification = useNotification();
|
|
23
|
-
const updatePw = useUpdatePassword();
|
|
23
|
+
const updatePw = useUpdatePassword({ successNotification: i18n.t('profile.passwordChanged') });
|
|
24
24
|
|
|
25
25
|
const authProvider = $derived(adminContext.authProvider);
|
|
26
26
|
const tenantIdentity = $derived(adminContext.tenantCacheKey?.__svadminTenant);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Ellipsis } from '@lucide/svelte';
|
|
3
|
-
import type
|
|
3
|
+
import { tick, type Component, type Snippet } from 'svelte';
|
|
4
4
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
5
|
import { cn, type WithElementRef } from '../utils.js';
|
|
6
6
|
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
children?: Snippet;
|
|
26
26
|
moreContent?: Snippet;
|
|
27
27
|
};
|
|
28
|
+
type RowActionsKeyboardEvent = KeyboardEvent & {
|
|
29
|
+
currentTarget: EventTarget & HTMLDivElement;
|
|
30
|
+
};
|
|
28
31
|
|
|
29
32
|
let {
|
|
30
33
|
actions = [],
|
|
@@ -32,6 +35,7 @@
|
|
|
32
35
|
moreLabel = 'More actions',
|
|
33
36
|
ref = $bindable(null),
|
|
34
37
|
class: className,
|
|
38
|
+
onkeydown,
|
|
35
39
|
children,
|
|
36
40
|
moreContent,
|
|
37
41
|
...restProps
|
|
@@ -46,6 +50,9 @@
|
|
|
46
50
|
);
|
|
47
51
|
const visibleActions = $derived(availableActions.slice(0, visibleLimit));
|
|
48
52
|
const overflowActions = $derived(availableActions.slice(visibleLimit));
|
|
53
|
+
let menuWasOpen = false;
|
|
54
|
+
let initialMenuFocus: 'first' | 'last' = 'first';
|
|
55
|
+
let restoreFocusOnClose = false;
|
|
49
56
|
|
|
50
57
|
function resolvedVariant(action: RowActionItem): RowActionItem['variant'] {
|
|
51
58
|
return action.variant ?? (action.danger ? 'destructive' : 'ghost');
|
|
@@ -56,15 +63,77 @@
|
|
|
56
63
|
event.preventDefault();
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
66
|
+
restoreFocusOnClose = true;
|
|
59
67
|
overflowOpen = false;
|
|
60
68
|
action.onclick?.(event);
|
|
61
69
|
}
|
|
70
|
+
|
|
71
|
+
function menuElements(): HTMLElement[] {
|
|
72
|
+
return [...(ref?.querySelector('[data-slot="dropdown-menu-content"]')?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])]
|
|
73
|
+
.filter((element) => !element.matches(':disabled, [aria-disabled="true"]'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function moveMenuFocus(event: KeyboardEvent, direction: 1 | -1): void {
|
|
77
|
+
const elements = menuElements();
|
|
78
|
+
if (!elements.length) return;
|
|
79
|
+
const currentIndex = elements.indexOf(document.activeElement as HTMLElement);
|
|
80
|
+
const nextIndex = currentIndex < 0
|
|
81
|
+
? (direction > 0 ? 0 : elements.length - 1)
|
|
82
|
+
: (currentIndex + direction + elements.length) % elements.length;
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
elements[nextIndex]?.focus();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function focusMenuBoundary(event: KeyboardEvent, boundary: 'first' | 'last'): void {
|
|
88
|
+
const elements = menuElements();
|
|
89
|
+
if (!elements.length) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
elements[boundary === 'first' ? 0 : elements.length - 1]?.focus();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function manageMenuKeyboard(event: RowActionsKeyboardEvent): void {
|
|
95
|
+
onkeydown?.(event);
|
|
96
|
+
if (event.defaultPrevented) return;
|
|
97
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
98
|
+
const trigger = target?.closest<HTMLElement>('[data-slot="dropdown-menu-trigger"]');
|
|
99
|
+
const content = target?.closest<HTMLElement>('[data-slot="dropdown-menu-content"]');
|
|
100
|
+
if (trigger && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
initialMenuFocus = event.key === 'ArrowUp' ? 'last' : 'first';
|
|
103
|
+
trigger.click();
|
|
104
|
+
} else if (content && event.key === 'Escape') {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
restoreFocusOnClose = true;
|
|
107
|
+
overflowOpen = false;
|
|
108
|
+
} else if (content && event.key === 'ArrowDown') moveMenuFocus(event, 1);
|
|
109
|
+
else if (content && event.key === 'ArrowUp') moveMenuFocus(event, -1);
|
|
110
|
+
else if (content && event.key === 'Home') focusMenuBoundary(event, 'first');
|
|
111
|
+
else if (content && event.key === 'End') focusMenuBoundary(event, 'last');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (overflowOpen) {
|
|
116
|
+
void tick().then(() => {
|
|
117
|
+
menuWasOpen = true;
|
|
118
|
+
const elements = menuElements();
|
|
119
|
+
elements[initialMenuFocus === 'last' ? elements.length - 1 : 0]?.focus();
|
|
120
|
+
initialMenuFocus = 'first';
|
|
121
|
+
});
|
|
122
|
+
} else if (menuWasOpen) {
|
|
123
|
+
menuWasOpen = false;
|
|
124
|
+
if (restoreFocusOnClose) {
|
|
125
|
+
restoreFocusOnClose = false;
|
|
126
|
+
ref?.querySelector<HTMLElement>('[data-slot="dropdown-menu-trigger"]')?.focus();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
62
130
|
</script>
|
|
63
131
|
|
|
64
132
|
<div
|
|
65
133
|
bind:this={ref}
|
|
66
134
|
data-slot="row-actions"
|
|
67
135
|
class={cn("inline-flex items-center gap-1.5", className)}
|
|
136
|
+
onkeydown={manageMenuKeyboard}
|
|
68
137
|
{...restProps}
|
|
69
138
|
>
|
|
70
139
|
{#if children}
|
|
@@ -109,12 +178,12 @@
|
|
|
109
178
|
<DropdownMenu.Root bind:open={overflowOpen}>
|
|
110
179
|
<DropdownMenu.Trigger>
|
|
111
180
|
{#snippet child({ props })}
|
|
112
|
-
<Button {...props} variant="ghost" size="icon-sm" title={moreLabel} aria-label={moreLabel}>
|
|
181
|
+
<Button {...props} variant="ghost" size="icon-sm" title={moreLabel} aria-label={moreLabel} aria-haspopup="menu" aria-expanded={overflowOpen}>
|
|
113
182
|
<Ellipsis class="size-4" />
|
|
114
183
|
</Button>
|
|
115
184
|
{/snippet}
|
|
116
185
|
</DropdownMenu.Trigger>
|
|
117
|
-
<DropdownMenu.Content align="end" class="min-w-[8rem]">
|
|
186
|
+
<DropdownMenu.Content align="end" class="min-w-[8rem]" role="menu" aria-label={moreLabel}>
|
|
118
187
|
{#each overflowActions as action (action)}
|
|
119
188
|
{#if action.href}
|
|
120
189
|
<Button
|
|
@@ -123,6 +192,8 @@
|
|
|
123
192
|
size="sm"
|
|
124
193
|
disabled={action.disabled}
|
|
125
194
|
onclick={(event) => handleOverflowAction(action, event)}
|
|
195
|
+
role="menuitem"
|
|
196
|
+
tabindex={-1}
|
|
126
197
|
class="h-auto w-full justify-start rounded-md px-2 py-1.5 text-sm font-normal"
|
|
127
198
|
>
|
|
128
199
|
{#if action.icon}
|
|
@@ -136,6 +207,8 @@
|
|
|
136
207
|
onclick={(event) => handleOverflowAction(action, event)}
|
|
137
208
|
disabled={action.disabled}
|
|
138
209
|
destructive={action.danger || action.variant === 'destructive'}
|
|
210
|
+
role="menuitem"
|
|
211
|
+
tabindex={-1}
|
|
139
212
|
>
|
|
140
213
|
{#if action.icon}
|
|
141
214
|
{@const Icon = action.icon}
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
let activeKey = $derived.by(() => {
|
|
111
111
|
const tab = getParams().tab;
|
|
112
112
|
if (!tab) return "profile";
|
|
113
|
-
const alias = tab === "api-keys" ? "api" : tab;
|
|
113
|
+
const alias = tab === "api-keys" ? "api" : tab === "audit-logs" ? "audit" : tab;
|
|
114
114
|
if (sectionKeys.has(alias)) return alias;
|
|
115
115
|
return content ? alias : "profile";
|
|
116
116
|
});
|
|
@@ -202,15 +202,26 @@
|
|
|
202
202
|
return () => { cancelled = true; };
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
const LOCALE_STORAGE_KEY = 'svadmin-locale';
|
|
206
|
+
|
|
207
|
+
function localeName(locale: string): string {
|
|
208
|
+
if (locale === 'zh-CN') return '简体中文';
|
|
209
|
+
if (locale === 'en') return 'English';
|
|
210
|
+
return locale;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
function selectLocale(locale: string): void {
|
|
214
|
+
i18n.setLocale(locale);
|
|
215
|
+
if (typeof window !== 'undefined') localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
$effect(() => {
|
|
219
|
+
if (typeof window === 'undefined') return;
|
|
220
|
+
const storedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
221
|
+
if (storedLocale && i18n.getAvailableLocales().includes(storedLocale)) {
|
|
222
|
+
i18n.setLocale(storedLocale);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
214
225
|
|
|
215
226
|
const path = $derived(getPath());
|
|
216
227
|
|
|
@@ -274,6 +285,19 @@
|
|
|
274
285
|
});
|
|
275
286
|
</script>
|
|
276
287
|
|
|
288
|
+
{#snippet languageMenu()}
|
|
289
|
+
<select
|
|
290
|
+
aria-label={i18n.t('common.switchLanguage')}
|
|
291
|
+
value={i18n.locale}
|
|
292
|
+
onchange={(event) => selectLocale((event.target as HTMLSelectElement).value)}
|
|
293
|
+
class="h-8 w-14 rounded-md border border-border bg-sidebar px-1 text-[11px] font-semibold text-sidebar-foreground/70 outline-none focus:ring-2 focus:ring-ring"
|
|
294
|
+
>
|
|
295
|
+
{#each i18n.getAvailableLocales() as locale (locale)}
|
|
296
|
+
<option value={locale}>{localeName(locale)}</option>
|
|
297
|
+
{/each}
|
|
298
|
+
</select>
|
|
299
|
+
{/snippet}
|
|
300
|
+
|
|
277
301
|
|
|
278
302
|
|
|
279
303
|
<aside
|
|
@@ -444,9 +468,7 @@
|
|
|
444
468
|
</div>
|
|
445
469
|
<div class="mt-1 flex items-center justify-between px-1">
|
|
446
470
|
<div class="flex items-center gap-0.5">
|
|
447
|
-
|
|
448
|
-
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
449
|
-
</TooltipButton>
|
|
471
|
+
{@render languageMenu()}
|
|
450
472
|
<TooltipButton tooltip={i18n.t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
451
473
|
{#if getResolvedTheme() === 'dark'}
|
|
452
474
|
<Sun class="h-3.5 w-3.5" />
|
|
@@ -465,16 +487,7 @@
|
|
|
465
487
|
</div>
|
|
466
488
|
{:else if collapsed}
|
|
467
489
|
<div class="flex flex-col items-center gap-0.5 px-2 py-3">
|
|
468
|
-
|
|
469
|
-
<Tooltip.Trigger>
|
|
470
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
471
|
-
<Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
472
|
-
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
473
|
-
</Button>
|
|
474
|
-
{/snippet}
|
|
475
|
-
</Tooltip.Trigger>
|
|
476
|
-
<Tooltip.Content side="right">{i18n.t('common.switchLanguage')}</Tooltip.Content>
|
|
477
|
-
</Tooltip.Root>
|
|
490
|
+
{@render languageMenu()}
|
|
478
491
|
<Tooltip.Root>
|
|
479
492
|
<Tooltip.Trigger>
|
|
480
493
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
@@ -512,9 +525,7 @@
|
|
|
512
525
|
</div>
|
|
513
526
|
{:else}
|
|
514
527
|
<div class="flex justify-center gap-1 p-3">
|
|
515
|
-
|
|
516
|
-
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
517
|
-
</TooltipButton>
|
|
528
|
+
{@render languageMenu()}
|
|
518
529
|
<TooltipButton tooltip={i18n.t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
519
530
|
{#if getResolvedTheme() === 'dark'}
|
|
520
531
|
<Sun class="h-3.5 w-3.5" />
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
activeId: string;
|
|
19
19
|
onselect?: (stage: WorkspaceStage) => void;
|
|
20
20
|
ariaLabel?: string;
|
|
21
|
+
statusLabels?: Partial<Record<WorkspaceStageStatus, string>>;
|
|
22
|
+
getStageAriaLabel?: (stage: WorkspaceStage) => string;
|
|
21
23
|
class?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
activeId,
|
|
27
29
|
onselect,
|
|
28
30
|
ariaLabel = 'Workflow stages',
|
|
31
|
+
statusLabels = {},
|
|
32
|
+
getStageAriaLabel,
|
|
29
33
|
class: className = '',
|
|
30
34
|
}: Props = $props();
|
|
31
35
|
|
|
@@ -35,6 +39,14 @@
|
|
|
35
39
|
blocked: 'border-warning/40 bg-warning/15 text-warning-foreground',
|
|
36
40
|
pending: 'border-border bg-background text-muted-foreground',
|
|
37
41
|
};
|
|
42
|
+
|
|
43
|
+
const resolvedStatusLabels = $derived<Record<WorkspaceStageStatus, string>>({
|
|
44
|
+
complete: 'Complete',
|
|
45
|
+
current: 'Current stage',
|
|
46
|
+
blocked: 'Blocked',
|
|
47
|
+
pending: 'Pending',
|
|
48
|
+
...statusLabels,
|
|
49
|
+
});
|
|
38
50
|
</script>
|
|
39
51
|
|
|
40
52
|
<nav aria-label={ariaLabel} class={cn('overflow-x-auto', className)} data-svadmin-workspace-stages>
|
|
@@ -45,7 +57,7 @@
|
|
|
45
57
|
type="button"
|
|
46
58
|
class="group grid min-w-28 grid-cols-[auto_minmax(0,1fr)] gap-x-2 rounded-md px-2 py-1.5 text-left outline-none transition-colors hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring sm:min-w-36"
|
|
47
59
|
aria-current={stage.id === activeId ? 'step' : undefined}
|
|
48
|
-
aria-label={`${stage.label}: ${stage.status}`}
|
|
60
|
+
aria-label={getStageAriaLabel?.(stage) || `${stage.label}: ${resolvedStatusLabels[stage.status]}`}
|
|
49
61
|
onclick={() => onselect?.(stage)}
|
|
50
62
|
>
|
|
51
63
|
<span class={cn('flex size-7 items-center justify-center rounded-full border', markerClass[stage.status])}>
|
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
activeId: string;
|
|
11
11
|
onselect?: (stage: WorkspaceStage) => void;
|
|
12
12
|
ariaLabel?: string;
|
|
13
|
+
statusLabels?: Partial<Record<WorkspaceStageStatus, string>>;
|
|
14
|
+
getStageAriaLabel?: (stage: WorkspaceStage) => string;
|
|
13
15
|
class?: string;
|
|
14
16
|
}
|
|
15
17
|
declare const WorkspaceStageStepper: import("svelte").Component<Props, {}, "">;
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
open?: boolean;
|
|
51
51
|
side?: "left" | "right";
|
|
52
52
|
onClose?: () => void;
|
|
53
|
+
closeLabel?: string;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
let {
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
open = $bindable(false),
|
|
58
59
|
side = "right",
|
|
59
60
|
onClose,
|
|
61
|
+
closeLabel = "Close",
|
|
60
62
|
class: className,
|
|
61
63
|
tabindex = -1,
|
|
62
64
|
children,
|
|
@@ -185,7 +187,7 @@
|
|
|
185
187
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
186
188
|
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
|
187
189
|
</svg>
|
|
188
|
-
<span class="sr-only">
|
|
190
|
+
<span class="sr-only">{closeLabel}</span>
|
|
189
191
|
</button>
|
|
190
192
|
</div>
|
|
191
193
|
{/if}
|
|
@@ -4,6 +4,7 @@ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
|
4
4
|
open?: boolean;
|
|
5
5
|
side?: "left" | "right";
|
|
6
6
|
onClose?: () => void;
|
|
7
|
+
closeLabel?: string;
|
|
7
8
|
};
|
|
8
9
|
declare const Sheet: import("svelte").Component<Props, {}, "open" | "ref">;
|
|
9
10
|
type Sheet = ReturnType<typeof Sheet>;
|
|
@@ -6,4 +6,5 @@ import Footer from "./table-footer.svelte";
|
|
|
6
6
|
import Head from "./table-head.svelte";
|
|
7
7
|
import Header from "./table-header.svelte";
|
|
8
8
|
import Row from "./table-row.svelte";
|
|
9
|
+
export type { TableDensity } from "./table.svelte";
|
|
9
10
|
export { Root, Body, Caption, Cell, Footer, Head, Header, Row, Root as Table, Body as TableBody, Caption as TableCaption, Cell as TableCell, Footer as TableFooter, Head as TableHead, Header as TableHeader, Row as TableRow, };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getContext } from "svelte";
|
|
2
3
|
import { cn, type WithElementRef } from "../../../utils.js";
|
|
3
4
|
import type { HTMLTdAttributes } from "svelte/elements";
|
|
5
|
+
import type { TableDensity } from "./table.svelte";
|
|
4
6
|
|
|
5
7
|
let {
|
|
6
8
|
ref = $bindable(null),
|
|
@@ -8,8 +10,13 @@
|
|
|
8
10
|
children,
|
|
9
11
|
...restProps
|
|
10
12
|
}: WithElementRef<HTMLTdAttributes, HTMLTableCellElement> = $props();
|
|
13
|
+
|
|
14
|
+
const getDensity = getContext<() => TableDensity>("svadmin-table-density");
|
|
15
|
+
const densityClass = $derived(getDensity?.() === "compact"
|
|
16
|
+
? "h-8 px-2 py-1 text-xs"
|
|
17
|
+
: "p-2");
|
|
11
18
|
</script>
|
|
12
19
|
|
|
13
|
-
<td bind:this={ref} data-slot="table-cell" class={cn("
|
|
20
|
+
<td bind:this={ref} data-slot="table-cell" class={cn("align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
|
|
14
21
|
{@render children?.()}
|
|
15
22
|
</td>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getContext } from "svelte";
|
|
2
3
|
import { cn, type WithElementRef } from "../../../utils.js";
|
|
3
4
|
import type { HTMLThAttributes } from "svelte/elements";
|
|
5
|
+
import type { TableDensity } from "./table.svelte";
|
|
4
6
|
|
|
5
7
|
let {
|
|
6
8
|
ref = $bindable(null),
|
|
@@ -8,8 +10,13 @@
|
|
|
8
10
|
children,
|
|
9
11
|
...restProps
|
|
10
12
|
}: WithElementRef<HTMLThAttributes, HTMLTableCellElement> = $props();
|
|
13
|
+
|
|
14
|
+
const getDensity = getContext<() => TableDensity>("svadmin-table-density");
|
|
15
|
+
const densityClass = $derived(getDensity?.() === "compact"
|
|
16
|
+
? "h-8 px-2 py-1 text-xs"
|
|
17
|
+
: "h-9 px-2");
|
|
11
18
|
</script>
|
|
12
19
|
|
|
13
|
-
<th bind:this={ref} data-slot="table-head" class={cn("text-foreground
|
|
20
|
+
<th bind:this={ref} data-slot="table-head" class={cn("text-foreground text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
|
|
14
21
|
{@render children?.()}
|
|
15
22
|
</th>
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { setContext } from "svelte";
|
|
2
3
|
import type { HTMLTableAttributes } from "svelte/elements";
|
|
3
4
|
import { cn, type WithElementRef } from "../../../utils.js";
|
|
4
5
|
|
|
6
|
+
export type TableDensity = "compact" | "comfortable";
|
|
7
|
+
|
|
8
|
+
type Props = WithElementRef<HTMLTableAttributes, HTMLTableElement> & {
|
|
9
|
+
density?: TableDensity;
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
let {
|
|
6
13
|
ref = $bindable(null),
|
|
14
|
+
density = "comfortable",
|
|
7
15
|
class: className,
|
|
8
16
|
children,
|
|
9
17
|
...restProps
|
|
10
|
-
}:
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
setContext("svadmin-table-density", () => density);
|
|
11
21
|
</script>
|
|
12
22
|
|
|
13
|
-
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
|
23
|
+
<div data-slot="table-container" data-table-density={density} class="relative w-full overflow-x-auto">
|
|
14
24
|
<table bind:this={ref} data-slot="table" class={cn("w-full caption-bottom text-sm", className)} {...restProps}>
|
|
15
25
|
{@render children?.()}
|
|
16
26
|
</table>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { HTMLTableAttributes } from "svelte/elements";
|
|
2
2
|
import { type WithElementRef } from "../../../utils.js";
|
|
3
|
-
|
|
3
|
+
export type TableDensity = "compact" | "comfortable";
|
|
4
|
+
type Props = WithElementRef<HTMLTableAttributes, HTMLTableElement> & {
|
|
5
|
+
density?: TableDensity;
|
|
6
|
+
};
|
|
7
|
+
declare const Table: import("svelte").Component<Props, {}, "ref">;
|
|
4
8
|
type Table = ReturnType<typeof Table>;
|
|
5
9
|
export default Table;
|
|
@@ -49,6 +49,8 @@ const ROUTES = [
|
|
|
49
49
|
'/:resource/edit/:id',
|
|
50
50
|
'/:resource/show/:id',
|
|
51
51
|
'/:resource/clone/:id',
|
|
52
|
+
'/:resource/:id/edit',
|
|
53
|
+
'/:resource/:id',
|
|
52
54
|
'/:parent/:parentId/:resource',
|
|
53
55
|
'/:parent/:parentId/:resource/create',
|
|
54
56
|
'/:parent/:parentId/:resource/edit/:id',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.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.
|
|
42
|
+
"@svadmin/core": "^0.40.1",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|