@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.
Files changed (34) hide show
  1. package/dist/components/AdminApp.svelte +40 -2
  2. package/dist/components/AutoTable.svelte +5 -3
  3. package/dist/components/AutoTable.svelte.d.ts +1 -0
  4. package/dist/components/DetailDrawer.svelte +14 -3
  5. package/dist/components/DetailDrawer.svelte.d.ts +3 -0
  6. package/dist/components/Layout.svelte +1 -0
  7. package/dist/components/ProfilePage.svelte +1 -1
  8. package/dist/components/RowActions.svelte +76 -3
  9. package/dist/components/RowActions.svelte.d.ts +1 -1
  10. package/dist/components/SettingsPage.svelte +1 -1
  11. package/dist/components/Sidebar.svelte +34 -23
  12. package/dist/components/content/WorkspaceActionBar.svelte +33 -0
  13. package/dist/components/content/WorkspaceActionBar.svelte.d.ts +12 -0
  14. package/dist/components/content/WorkspaceInspector.svelte +57 -0
  15. package/dist/components/content/WorkspaceInspector.svelte.d.ts +15 -0
  16. package/dist/components/content/WorkspaceLayout.svelte +12 -1
  17. package/dist/components/content/WorkspaceLayout.svelte.d.ts +2 -0
  18. package/dist/components/content/WorkspaceSplitPane.svelte +34 -0
  19. package/dist/components/content/WorkspaceSplitPane.svelte.d.ts +11 -0
  20. package/dist/components/content/WorkspaceStageStepper.svelte +83 -0
  21. package/dist/components/content/WorkspaceStageStepper.svelte.d.ts +19 -0
  22. package/dist/components/content/index.d.ts +4 -0
  23. package/dist/components/content/index.js +4 -0
  24. package/dist/components/ui/sheet/sheet.svelte +3 -1
  25. package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
  26. package/dist/components/ui/table/index.d.ts +1 -0
  27. package/dist/components/ui/table/table-cell.svelte +8 -1
  28. package/dist/components/ui/table/table-head.svelte +8 -1
  29. package/dist/components/ui/table/table.svelte +12 -2
  30. package/dist/components/ui/table/table.svelte.d.ts +5 -1
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.js +4 -0
  33. package/dist/router-state.svelte.js +2 -0
  34. 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="rounded-[24px] bg-card/80 backdrop-blur-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] overflow-hidden ring-1 ring-border/30" role="region" aria-label="{resource.label} {i18n.t('common.list')}">
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("bg-transparent hover:bg-muted/10 border-b border-border/20 uppercase tracking-[0.12em] text-[10px] text-muted-foreground/70 font-semibold py-4", dragProps.class)}
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'}
@@ -3,6 +3,7 @@ import type { Snippet } from 'svelte';
3
3
  interface Props {
4
4
  resourceName: string;
5
5
  selectable?: boolean;
6
+ density?: 'compact' | 'comfortable';
6
7
  /** Custom header actions (right side) */
7
8
  headerActions?: Snippet;
8
9
  /** Custom cell renderers by field key */
@@ -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;
@@ -188,6 +188,7 @@
188
188
  {siteUrl}
189
189
  {menu}
190
190
  showSearch={true}
191
+ showThemeToggle={true}
191
192
  onSearchClick={() => { commandOpen = true; }}
192
193
  >
193
194
  {#snippet children()}
@@ -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 { Component, Snippet } from 'svelte';
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}
@@ -1,4 +1,4 @@
1
- import type { Component, Snippet } from 'svelte';
1
+ import { type Component, type Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { type WithElementRef } from '../utils.js';
4
4
  export type RowActionItem = {
@@ -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
- function toggleLocale() {
206
- const locales = i18n.getAvailableLocales();
207
- const current = i18n.locale;
208
- const idx = locales.indexOf(current);
209
- const next = locales[(idx + 1) % locales.length];
210
- i18n.setLocale(next);
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
- const localeLabel = $derived(i18n.locale === 'zh-CN' ? '中' : 'EN');
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
- <TooltipButton tooltip={i18n.t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
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
- <Tooltip.Root>
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
- <TooltipButton tooltip={i18n.t('common.switchLanguage')} 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">
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 class={cn('flex flex-col gap-6', className)} data-svadmin-workspace-layout style:--workspace-secondary-width={secondaryWidth}>
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}
@@ -4,6 +4,8 @@ interface Props {
4
4
  secondary?: Snippet;
5
5
  summary?: Snippet;
6
6
  secondaryWidth?: string;
7
+ secondaryCollapsed?: boolean;
8
+ secondaryCollapsedWidth?: string;
7
9
  mobileOrder?: 'primary-first' | 'secondary-first';
8
10
  class?: string;
9
11
  }
@@ -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">Close</span>
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("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} {...restProps}>
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 h-9 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} {...restProps}>
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
- }: WithElementRef<HTMLTableAttributes, HTMLTableElement> = $props();
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
- declare const Table: import("svelte").Component<WithElementRef<HTMLTableAttributes, HTMLTableElement>, {}, "ref">;
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.48.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.0",
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",