@svadmin/ui 0.48.0 → 0.49.1
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 +5 -3
- package/dist/components/AutoTable.svelte.d.ts +1 -0
- 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/WorkspaceActionBar.svelte +33 -0
- package/dist/components/content/WorkspaceActionBar.svelte.d.ts +12 -0
- package/dist/components/content/WorkspaceInspector.svelte +57 -0
- package/dist/components/content/WorkspaceInspector.svelte.d.ts +15 -0
- package/dist/components/content/WorkspaceLayout.svelte +12 -1
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +2 -0
- package/dist/components/content/WorkspaceSplitPane.svelte +34 -0
- package/dist/components/content/WorkspaceSplitPane.svelte.d.ts +11 -0
- package/dist/components/content/WorkspaceStageStepper.svelte +83 -0
- package/dist/components/content/WorkspaceStageStepper.svelte.d.ts +19 -0
- package/dist/components/content/index.d.ts +4 -0
- package/dist/components/content/index.js +4 -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/index.d.ts +5 -0
- package/dist/index.js +4 -0
- 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} />
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
interface Props {
|
|
76
76
|
resourceName: string;
|
|
77
77
|
selectable?: boolean;
|
|
78
|
+
density?: 'compact' | 'comfortable';
|
|
78
79
|
/** Custom header actions (right side) */
|
|
79
80
|
headerActions?: Snippet;
|
|
80
81
|
/** Custom cell renderers by field key */
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
let {
|
|
99
100
|
resourceName,
|
|
100
101
|
selectable = true,
|
|
102
|
+
density = 'comfortable',
|
|
101
103
|
headerActions,
|
|
102
104
|
columns: customColumns,
|
|
103
105
|
defaultCellRenderer,
|
|
@@ -682,7 +684,7 @@
|
|
|
682
684
|
</div>
|
|
683
685
|
|
|
684
686
|
<!-- Table (TanStack-powered) -->
|
|
685
|
-
<div class="
|
|
687
|
+
<div class="overflow-hidden rounded-lg border border-border bg-card shadow-sm" role="region" aria-label="{resource.label} {i18n.t('common.list')}" data-table-density={density}>
|
|
686
688
|
{#if query.isLoading}
|
|
687
689
|
<div class="p-4 space-y-3">
|
|
688
690
|
<div class="flex gap-4 mb-2">
|
|
@@ -706,7 +708,7 @@
|
|
|
706
708
|
<div in:fade={{ duration: 150 }}>
|
|
707
709
|
<!-- Desktop Table (hidden on mobile) -->
|
|
708
710
|
<div class="hidden md:block">
|
|
709
|
-
<Table.Root>
|
|
711
|
+
<Table.Root {density}>
|
|
710
712
|
<Table.Header>
|
|
711
713
|
{#each tableView.headerGroups as headerGroup, _i (_i)}
|
|
712
714
|
{@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, BaseRecord, unknown>) => isColumnVisible(header.column.id))}
|
|
@@ -719,7 +721,7 @@
|
|
|
719
721
|
{@const header = col.header as typeof headerGroup.headers[0]}
|
|
720
722
|
<Table.Head
|
|
721
723
|
{...dragProps}
|
|
722
|
-
class={cn(
|
|
724
|
+
class={cn('border-b border-border bg-muted/25 font-medium tracking-normal text-muted-foreground hover:bg-muted/40', dragProps.class)}
|
|
723
725
|
style={header_getSize(header) != null && header_getSize(header) !== 150 ? `width:${header_getSize(header)}px` : undefined}
|
|
724
726
|
>
|
|
725
727
|
{#if header.id === '_select'}
|
|
@@ -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" />
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
primaryAction: Snippet;
|
|
10
|
+
secondaryActions?: Snippet;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { title, description, status, primaryAction, secondaryActions, class: className = '' }: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<section
|
|
18
|
+
class={cn('flex flex-col gap-3 border-y border-border bg-card px-4 py-3 sm:flex-row sm:items-center sm:justify-between', className)}
|
|
19
|
+
aria-label={title}
|
|
20
|
+
data-svadmin-workspace-action-bar
|
|
21
|
+
>
|
|
22
|
+
<div class="min-w-0">
|
|
23
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
24
|
+
{#if status}<span class="rounded-sm bg-warning/15 px-2 py-0.5 text-xs font-medium text-warning-foreground">{status}</span>{/if}
|
|
25
|
+
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
26
|
+
</div>
|
|
27
|
+
{#if description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>{/if}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
|
30
|
+
{#if secondaryActions}{@render secondaryActions()}{/if}
|
|
31
|
+
{@render primaryAction()}
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
primaryAction: Snippet;
|
|
7
|
+
secondaryActions?: Snippet;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const WorkspaceActionBar: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type WorkspaceActionBar = ReturnType<typeof WorkspaceActionBar>;
|
|
12
|
+
export default WorkspaceActionBar;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { PanelRightClose, PanelRightOpen } from '@lucide/svelte';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { cn } from '../../utils.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
open?: boolean;
|
|
11
|
+
ontoggle?: (open: boolean) => void;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
footer?: Snippet;
|
|
14
|
+
openLabel?: string;
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
open = true,
|
|
23
|
+
ontoggle,
|
|
24
|
+
children,
|
|
25
|
+
footer,
|
|
26
|
+
openLabel = 'Open inspector',
|
|
27
|
+
closeLabel = 'Close inspector',
|
|
28
|
+
class: className = '',
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
function toggle(): void {
|
|
32
|
+
ontoggle?.(!open);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<aside class={cn('overflow-hidden rounded-lg border border-border bg-card', className)} data-svadmin-workspace-inspector data-open={open}>
|
|
37
|
+
{#if open}
|
|
38
|
+
<header class="flex items-start justify-between gap-3 border-b border-border px-4 py-3">
|
|
39
|
+
<div class="min-w-0">
|
|
40
|
+
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
41
|
+
{#if description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>{/if}
|
|
42
|
+
</div>
|
|
43
|
+
<Button variant="ghost" size="icon-sm" aria-label={closeLabel} title={closeLabel} onclick={toggle}>
|
|
44
|
+
<PanelRightClose class="size-4" />
|
|
45
|
+
</Button>
|
|
46
|
+
</header>
|
|
47
|
+
<div class="max-h-[calc(100vh-15rem)] overflow-y-auto p-4">{@render children()}</div>
|
|
48
|
+
{#if footer}<footer class="border-t border-border p-3">{@render footer()}</footer>{/if}
|
|
49
|
+
{:else}
|
|
50
|
+
<div class="flex min-h-64 flex-col items-center gap-3 py-3">
|
|
51
|
+
<Button variant="ghost" size="icon-sm" aria-label={openLabel} title={openLabel} onclick={toggle}>
|
|
52
|
+
<PanelRightOpen class="size-4" />
|
|
53
|
+
</Button>
|
|
54
|
+
<span class="[writing-mode:vertical-rl] text-xs font-medium text-muted-foreground">{title}</span>
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</aside>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
ontoggle?: (open: boolean) => void;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
footer?: Snippet;
|
|
9
|
+
openLabel?: string;
|
|
10
|
+
closeLabel?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const WorkspaceInspector: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type WorkspaceInspector = ReturnType<typeof WorkspaceInspector>;
|
|
15
|
+
export default WorkspaceInspector;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
secondary?: Snippet;
|
|
8
8
|
summary?: Snippet;
|
|
9
9
|
secondaryWidth?: string;
|
|
10
|
+
secondaryCollapsed?: boolean;
|
|
11
|
+
secondaryCollapsedWidth?: string;
|
|
10
12
|
mobileOrder?: 'primary-first' | 'secondary-first';
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
@@ -16,12 +18,21 @@
|
|
|
16
18
|
secondary,
|
|
17
19
|
summary,
|
|
18
20
|
secondaryWidth = '22rem',
|
|
21
|
+
secondaryCollapsed = false,
|
|
22
|
+
secondaryCollapsedWidth = '3rem',
|
|
19
23
|
mobileOrder = 'primary-first',
|
|
20
24
|
class: className = '',
|
|
21
25
|
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const resolvedSecondaryWidth = $derived(secondaryCollapsed ? secondaryCollapsedWidth : secondaryWidth);
|
|
22
28
|
</script>
|
|
23
29
|
|
|
24
|
-
<div
|
|
30
|
+
<div
|
|
31
|
+
class={cn('flex flex-col gap-6', className)}
|
|
32
|
+
data-svadmin-workspace-layout
|
|
33
|
+
data-secondary-collapsed={secondaryCollapsed}
|
|
34
|
+
style:--workspace-secondary-width={resolvedSecondaryWidth}
|
|
35
|
+
>
|
|
25
36
|
{#if summary}<div data-svadmin-workspace-summary>{@render summary()}</div>{/if}
|
|
26
37
|
<div class={cn('grid min-w-0 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_var(--workspace-secondary-width)]', mobileOrder === 'secondary-first' && 'grid-rows-[auto_auto]')}>
|
|
27
38
|
{#if mobileOrder === 'secondary-first' && secondary}<aside class="order-1 min-w-0 xl:order-2">{@render secondary()}</aside>{/if}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
primary: Snippet;
|
|
7
|
+
secondary: Snippet;
|
|
8
|
+
secondaryWidth?: string;
|
|
9
|
+
reverse?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
primary,
|
|
15
|
+
secondary,
|
|
16
|
+
secondaryWidth = '42%',
|
|
17
|
+
reverse = false,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
class={cn(
|
|
24
|
+
'grid min-w-0 overflow-hidden rounded-lg border border-border bg-card',
|
|
25
|
+
reverse ? 'lg:grid-cols-[var(--workspace-split-width)_minmax(0,1fr)]' : 'lg:grid-cols-[minmax(0,1fr)_var(--workspace-split-width)]',
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
style:--workspace-split-width={secondaryWidth}
|
|
29
|
+
data-svadmin-workspace-split-pane
|
|
30
|
+
data-reverse={reverse}
|
|
31
|
+
>
|
|
32
|
+
<section class={cn('min-w-0 p-4', reverse && 'lg:order-2')} data-svadmin-split-primary>{@render primary()}</section>
|
|
33
|
+
<section class={cn('min-w-0 border-t border-border bg-muted/15 p-4 lg:border-l lg:border-t-0', reverse && 'lg:order-1 lg:border-l-0 lg:border-r')} data-svadmin-split-secondary>{@render secondary()}</section>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
primary: Snippet;
|
|
4
|
+
secondary: Snippet;
|
|
5
|
+
secondaryWidth?: string;
|
|
6
|
+
reverse?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const WorkspaceSplitPane: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type WorkspaceSplitPane = ReturnType<typeof WorkspaceSplitPane>;
|
|
11
|
+
export default WorkspaceSplitPane;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type WorkspaceStageStatus = 'complete' | 'current' | 'blocked' | 'pending';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceStage {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status: WorkspaceStageStatus;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { AlertTriangle, Check, Circle } from '@lucide/svelte';
|
|
14
|
+
import { cn } from '../../utils.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
stages: WorkspaceStage[];
|
|
18
|
+
activeId: string;
|
|
19
|
+
onselect?: (stage: WorkspaceStage) => void;
|
|
20
|
+
ariaLabel?: string;
|
|
21
|
+
statusLabels?: Partial<Record<WorkspaceStageStatus, string>>;
|
|
22
|
+
getStageAriaLabel?: (stage: WorkspaceStage) => string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
stages,
|
|
28
|
+
activeId,
|
|
29
|
+
onselect,
|
|
30
|
+
ariaLabel = 'Workflow stages',
|
|
31
|
+
statusLabels = {},
|
|
32
|
+
getStageAriaLabel,
|
|
33
|
+
class: className = '',
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const markerClass: Record<WorkspaceStageStatus, string> = {
|
|
37
|
+
complete: 'border-success/30 bg-success/10 text-success',
|
|
38
|
+
current: 'border-primary bg-primary text-primary-foreground',
|
|
39
|
+
blocked: 'border-warning/40 bg-warning/15 text-warning-foreground',
|
|
40
|
+
pending: 'border-border bg-background text-muted-foreground',
|
|
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
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<nav aria-label={ariaLabel} class={cn('overflow-x-auto', className)} data-svadmin-workspace-stages>
|
|
53
|
+
<ol class="flex min-w-max items-start gap-0 px-0.5 py-1">
|
|
54
|
+
{#each stages as stage, index (stage.id)}
|
|
55
|
+
<li class="flex items-start">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
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"
|
|
59
|
+
aria-current={stage.id === activeId ? 'step' : undefined}
|
|
60
|
+
aria-label={getStageAriaLabel?.(stage) || `${stage.label}: ${resolvedStatusLabels[stage.status]}`}
|
|
61
|
+
onclick={() => onselect?.(stage)}
|
|
62
|
+
>
|
|
63
|
+
<span class={cn('flex size-7 items-center justify-center rounded-full border', markerClass[stage.status])}>
|
|
64
|
+
{#if stage.status === 'complete'}
|
|
65
|
+
<Check class="size-3.5" />
|
|
66
|
+
{:else if stage.status === 'blocked'}
|
|
67
|
+
<AlertTriangle class="size-3.5" />
|
|
68
|
+
{:else}
|
|
69
|
+
<Circle class={cn('size-2.5', stage.status === 'current' && 'fill-current')} />
|
|
70
|
+
{/if}
|
|
71
|
+
</span>
|
|
72
|
+
<span class="min-w-0 pt-0.5">
|
|
73
|
+
<span class={cn('block text-xs font-medium', stage.id === activeId ? 'text-foreground' : 'text-muted-foreground')}>{stage.label}</span>
|
|
74
|
+
{#if stage.description}<span class="mt-0.5 block max-w-32 truncate text-[0.6875rem] text-muted-foreground">{stage.description}</span>{/if}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
{#if index < stages.length - 1}
|
|
78
|
+
<span class={cn('mt-5 h-px w-4 bg-border sm:w-7', stage.status === 'complete' && 'bg-success/50')} aria-hidden="true"></span>
|
|
79
|
+
{/if}
|
|
80
|
+
</li>
|
|
81
|
+
{/each}
|
|
82
|
+
</ol>
|
|
83
|
+
</nav>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type WorkspaceStageStatus = 'complete' | 'current' | 'blocked' | 'pending';
|
|
2
|
+
export interface WorkspaceStage {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
status: WorkspaceStageStatus;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
stages: WorkspaceStage[];
|
|
10
|
+
activeId: string;
|
|
11
|
+
onselect?: (stage: WorkspaceStage) => void;
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
statusLabels?: Partial<Record<WorkspaceStageStatus, string>>;
|
|
14
|
+
getStageAriaLabel?: (stage: WorkspaceStage) => string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const WorkspaceStageStepper: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type WorkspaceStageStepper = ReturnType<typeof WorkspaceStageStepper>;
|
|
19
|
+
export default WorkspaceStageStepper;
|
|
@@ -3,6 +3,10 @@ export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
|
+
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
|
+
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
6
10
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
11
|
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
8
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
@@ -3,6 +3,10 @@ export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
|
+
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
|
+
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
6
10
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
11
|
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
8
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
@@ -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;
|
package/dist/index.d.ts
CHANGED
|
@@ -159,6 +159,11 @@ export { default as ContentPageHeader } from './components/content/ContentPageHe
|
|
|
159
159
|
export { default as SectionHeader } from './components/content/SectionHeader.svelte';
|
|
160
160
|
export { default as PageToolbar } from './components/content/PageToolbar.svelte';
|
|
161
161
|
export { default as WorkspaceLayout } from './components/content/WorkspaceLayout.svelte';
|
|
162
|
+
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
163
|
+
export type { WorkspaceStage, WorkspaceStageStatus } from './components/content/WorkspaceStageStepper.svelte';
|
|
164
|
+
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
165
|
+
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
166
|
+
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
162
167
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
163
168
|
export { default as SettingsFieldRow } from './components/content/SettingsFieldRow.svelte';
|
|
164
169
|
export { default as MetricBlock } from './components/content/MetricBlock.svelte';
|
package/dist/index.js
CHANGED
|
@@ -174,6 +174,10 @@ export { default as ContentPageHeader } from './components/content/ContentPageHe
|
|
|
174
174
|
export { default as SectionHeader } from './components/content/SectionHeader.svelte';
|
|
175
175
|
export { default as PageToolbar } from './components/content/PageToolbar.svelte';
|
|
176
176
|
export { default as WorkspaceLayout } from './components/content/WorkspaceLayout.svelte';
|
|
177
|
+
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
178
|
+
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
179
|
+
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
180
|
+
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
177
181
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
178
182
|
export { default as SettingsFieldRow } from './components/content/SettingsFieldRow.svelte';
|
|
179
183
|
export { default as MetricBlock } from './components/content/MetricBlock.svelte';
|
|
@@ -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.
|
|
3
|
+
"version": "0.49.1",
|
|
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.39.
|
|
42
|
+
"@svadmin/core": "^0.39.1",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|