@svadmin/ui 0.45.0 → 0.47.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 (59) hide show
  1. package/dist/components/AboutSettings.svelte +21 -96
  2. package/dist/components/ApiSettings.svelte +54 -234
  3. package/dist/components/AppearanceSettings.svelte +36 -165
  4. package/dist/components/AuthPageShell.svelte +32 -0
  5. package/dist/components/AuthPageShell.svelte.d.ts +11 -0
  6. package/dist/components/AutoForm.svelte +21 -3
  7. package/dist/components/DetailDrawer.svelte +74 -0
  8. package/dist/components/DetailDrawer.svelte.d.ts +16 -0
  9. package/dist/components/FieldRenderer.svelte +70 -2
  10. package/dist/components/FieldRenderer.svelte.d.ts +2 -0
  11. package/dist/components/ForgotPasswordPage.svelte +4 -68
  12. package/dist/components/IntegrationsSettings.svelte +22 -61
  13. package/dist/components/LoginPage.svelte +33 -124
  14. package/dist/components/NotificationsSettings.svelte +22 -103
  15. package/dist/components/PermissionMatrix.svelte +18 -9
  16. package/dist/components/RegisterPage.svelte +7 -62
  17. package/dist/components/RowActions.svelte +154 -0
  18. package/dist/components/RowActions.svelte.d.ts +26 -0
  19. package/dist/components/SecuritySettings.svelte +18 -58
  20. package/dist/components/SettingsPage.svelte +12 -2
  21. package/dist/components/SvadminLogo.svelte +1 -1
  22. package/dist/components/UpdatePasswordPage.svelte +3 -55
  23. package/dist/components/account/CompanyProfilePage.svelte +19 -7
  24. package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
  25. package/dist/components/account/SettingsPlainPage.svelte +25 -3
  26. package/dist/components/account/SettingsSidebarPage.svelte +31 -2
  27. package/dist/components/account/UserProfilePage.svelte +5 -4
  28. package/dist/components/content/ApiKeyList.svelte +40 -6
  29. package/dist/components/content/ApiKeyList.svelte.d.ts +10 -0
  30. package/dist/components/content/DataState.svelte +25 -9
  31. package/dist/components/content/DataState.svelte.d.ts +2 -0
  32. package/dist/components/content/FileList.svelte +12 -4
  33. package/dist/components/content/FileList.svelte.d.ts +9 -0
  34. package/dist/components/content/FilterToolbar.svelte +10 -2
  35. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
  36. package/dist/components/content/MemberList.svelte +13 -5
  37. package/dist/components/content/MemberList.svelte.d.ts +9 -0
  38. package/dist/components/content/MetricBlock.svelte +19 -2
  39. package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
  40. package/dist/components/content/NetworkTable.svelte +9 -3
  41. package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
  42. package/dist/components/content/NetworkUserCard.svelte +47 -7
  43. package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
  44. package/dist/components/content/SectionHeader.svelte +3 -2
  45. package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
  46. package/dist/components/content/SecurityEventTable.svelte +14 -5
  47. package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
  48. package/dist/components/content/SettingsFieldRow.svelte +28 -0
  49. package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
  50. package/dist/components/content/SettingsGroup.svelte +33 -0
  51. package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
  52. package/dist/components/content/WorkspaceLayout.svelte +31 -0
  53. package/dist/components/content/WorkspaceLayout.svelte.d.ts +12 -0
  54. package/dist/components/content/index.d.ts +4 -1
  55. package/dist/components/content/index.js +3 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte +25 -6
  57. package/dist/index.d.ts +9 -1
  58. package/dist/index.js +6 -0
  59. package/package.json +4 -1
@@ -1,187 +1,58 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
-
4
3
  import { getTheme, setTheme, getColorTheme, setColorTheme, getColorPresets } from '@svadmin/core';
5
- import * as Card from './ui/card/index.js';
6
- import { Sun, Moon, Monitor, Check } from '@lucide/svelte';
4
+ import { Check, Monitor, Moon, Sun } from '@lucide/svelte';
5
+ import SettingsGroup from './content/SettingsGroup.svelte';
6
+ import SettingsFieldRow from './content/SettingsFieldRow.svelte';
7
7
 
8
8
  const i18n = useTranslation();
9
-
10
- // Theme mode
11
9
  const themeOptions = [
12
10
  { value: 'light' as const, icon: Sun, label: 'settings.light' },
13
11
  { value: 'dark' as const, icon: Moon, label: 'settings.dark' },
14
12
  { value: 'system' as const, icon: Monitor, label: 'settings.systemTheme' },
15
13
  ];
16
-
17
14
  let currentTheme = $derived(getTheme());
18
15
  let currentColor = $derived(getColorTheme());
19
16
  let currentLocale = $derived(i18n.locale);
20
17
  const availableLocales = i18n.getAvailableLocales();
21
18
  const presets = getColorPresets();
22
-
23
- // Sidebar density
24
19
  const DENSITY_KEY = 'svadmin-sidebar-density';
25
- let density = $state<'compact' | 'standard'>(
26
- (typeof window !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null) as 'compact' | 'standard' ?? 'standard'
27
- );
28
-
29
- function setDensity(d: 'compact' | 'standard') {
30
- density = d;
31
- if (typeof window !== 'undefined') {
32
- localStorage.setItem(DENSITY_KEY, d);
33
- window.dispatchEvent(new CustomEvent('svadmin-density-change', { detail: d }));
34
- }
35
- }
36
-
37
- // Page size
20
+ let density = $state<'compact' | 'standard'>((typeof window !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null) as 'compact' | 'standard' ?? 'standard');
38
21
  const PAGE_SIZE_KEY = 'svadmin-default-page-size';
39
22
  const pageSizeOptions = [10, 20, 50];
40
- let pageSize = $state<number>(
41
- typeof window !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10
42
- );
43
-
44
- function setPageSize(size: number) {
45
- pageSize = size;
46
- if (typeof window !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size));
47
- }
48
-
49
- // Locale display names
50
- const localeNames: Record<string, string> = {
51
- 'zh-CN': '中文(简体)',
52
- 'en': 'English',
53
- 'ja': '日本語',
54
- 'ko': '한국어',
55
- };
23
+ let pageSize = $state(typeof window !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10);
24
+ const localeNames: Record<string, string> = { 'zh-CN': '中文(简体)', en: 'English', ja: '日本語', ko: '한국어' };
25
+ function setDensity(value: 'compact' | 'standard') { density = value; if (typeof window !== 'undefined') { localStorage.setItem(DENSITY_KEY, value); window.dispatchEvent(new CustomEvent('svadmin-density-change', { detail: value })); } }
26
+ function setPageSize(size: number) { pageSize = size; if (typeof window !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size)); }
56
27
  </script>
57
28
 
58
29
  <div class="space-y-6">
59
- <div>
60
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.appearance')}</h2>
61
- <p class="text-sm text-muted-foreground mt-1">{i18n.t('settings.settingsDescription')}</p>
62
- </div>
63
-
64
- <!-- Theme Mode -->
65
- <Card.Card>
66
- <Card.CardHeader class="pb-3">
67
- <Card.CardTitle class="text-base">{i18n.t('settings.themeMode')}</Card.CardTitle>
68
- </Card.CardHeader>
69
- <Card.CardContent>
70
- <div class="grid grid-cols-3 gap-3">
71
- {#each themeOptions as opt, _i (_i)}
72
- {@const active = currentTheme === opt.value}
73
- <button
74
- class="relative flex flex-col items-center gap-2 rounded-xl border-2 p-4 transition-all duration-200
75
- {active
76
- ? 'border-primary bg-primary/5 shadow-sm'
77
- : 'border-border hover:border-muted-foreground/30 hover:bg-accent/50'}"
78
- onclick={() => setTheme(opt.value)}
79
- >
80
- {#if active}
81
- <div class="absolute top-2 right-2">
82
- <Check class="h-3.5 w-3.5 text-primary" />
83
- </div>
84
- {/if}
85
- <div class="h-10 w-10 rounded-lg flex items-center justify-center
86
- {active ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}">
87
- <opt.icon class="h-5 w-5" />
88
- </div>
89
- <span class="text-xs font-medium {active ? 'text-primary' : 'text-muted-foreground'}">
90
- {i18n.t(opt.label)}
91
- </span>
92
- </button>
93
- {/each}
94
- </div>
95
- </Card.CardContent>
96
- </Card.Card>
97
-
98
- <!-- Color Accent -->
99
- <Card.Card>
100
- <Card.CardHeader class="pb-3">
101
- <Card.CardTitle class="text-base">{i18n.t('settings.colorAccent')}</Card.CardTitle>
102
- </Card.CardHeader>
103
- <Card.CardContent>
104
- <div class="flex flex-wrap gap-3">
105
- {#each presets as preset, _i (_i)}
106
- {@const active = currentColor === preset.name}
107
- <button
108
- class="group relative h-9 w-9 rounded-full transition-all duration-200 hover:scale-110
109
- {active ? 'ring-2 ring-offset-2 ring-offset-background scale-110' : 'opacity-70 hover:opacity-100'}"
110
- style="background-color: {preset.color}; {active ? `--tw-ring-color: ${preset.color}` : ''}"
111
- onclick={() => setColorTheme(preset.name as Parameters<typeof setColorTheme>[0])}
112
- title={preset.label}
113
- >
114
- {#if active}
115
- <Check class="h-4 w-4 text-white absolute inset-0 m-auto drop-shadow-sm" />
116
- {/if}
117
- </button>
118
- {/each}
119
- </div>
120
- </Card.CardContent>
121
- </Card.Card>
122
-
123
- <!-- Interface -->
124
- <Card.Card>
125
- <Card.CardHeader class="pb-3">
126
- <Card.CardTitle class="text-base">{i18n.t('settings.interface')}</Card.CardTitle>
127
- </Card.CardHeader>
128
- <Card.CardContent class="space-y-5">
129
- <!-- Language -->
130
- <div class="flex items-center justify-between">
131
- <div>
132
- <p class="text-sm font-medium text-foreground">{i18n.t('settings.language')}</p>
133
- <p class="text-xs text-muted-foreground">{localeNames[currentLocale] ?? currentLocale}</p>
134
- </div>
135
- <select
136
- class="h-9 rounded-md border border-input bg-background px-3 text-sm ring-offset-background
137
- focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
138
- value={currentLocale}
139
- onchange={(e) => i18n.setLocale((e.target as HTMLSelectElement).value)}
140
- >
141
- {#each availableLocales as loc, _i (_i)}
142
- <option value={loc}>{localeNames[loc] ?? loc}</option>
143
- {/each}
144
- </select>
145
- </div>
146
-
147
- <div class="h-px bg-border"></div>
148
-
149
- <!-- Sidebar Density -->
150
- <div class="flex items-center justify-between">
151
- <div>
152
- <p class="text-sm font-medium text-foreground">{i18n.t('settings.sidebarDensity')}</p>
153
- </div>
154
- <div class="flex rounded-lg border border-input overflow-hidden">
155
- <button
156
- class="px-3 py-1.5 text-xs font-medium transition-colors
157
- {density === 'compact' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
158
- onclick={() => setDensity('compact')}
159
- >{i18n.t('settings.compact')}</button>
160
- <button
161
- class="px-3 py-1.5 text-xs font-medium transition-colors
162
- {density === 'standard' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
163
- onclick={() => setDensity('standard')}
164
- >{i18n.t('settings.standard')}</button>
165
- </div>
166
- </div>
167
-
168
- <div class="h-px bg-border"></div>
169
-
170
- <!-- Default Page Size -->
171
- <div class="flex items-center justify-between">
172
- <div>
173
- <p class="text-sm font-medium text-foreground">{i18n.t('settings.defaultPageSize')}</p>
174
- </div>
175
- <div class="flex rounded-lg border border-input overflow-hidden">
176
- {#each pageSizeOptions as size, _i (_i)}
177
- <button
178
- class="px-3 py-1.5 text-xs font-mono font-medium transition-colors
179
- {pageSize === size ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
180
- onclick={() => setPageSize(size)}
181
- >{size}</button>
182
- {/each}
183
- </div>
184
- </div>
185
- </Card.CardContent>
186
- </Card.Card>
30
+ <div><h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.appearance')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.settingsDescription')}</p></div>
31
+ <SettingsGroup title={i18n.t('settings.themeMode')}>
32
+ <div class="grid grid-cols-3 gap-3">
33
+ {#each themeOptions as option (option.value)}
34
+ {@const active = currentTheme === option.value}
35
+ <button class="relative flex min-w-0 flex-col items-center gap-2 rounded-lg border p-4 transition-colors {active ? 'border-primary bg-primary/5' : 'border-border hover:bg-accent/50'}" onclick={() => setTheme(option.value)} aria-pressed={active}>
36
+ {#if active}<Check class="absolute right-2 top-2 size-3.5 text-primary" />{/if}
37
+ <span class="flex size-10 items-center justify-center rounded-md {active ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}"><option.icon class="size-5" /></span>
38
+ <span class="truncate text-xs font-medium {active ? 'text-primary' : 'text-muted-foreground'}">{i18n.t(option.label)}</span>
39
+ </button>
40
+ {/each}
41
+ </div>
42
+ </SettingsGroup>
43
+ <SettingsGroup title={i18n.t('settings.colorAccent')}>
44
+ <div class="flex flex-wrap gap-3">
45
+ {#each presets as preset (preset.name)}
46
+ {@const active = currentColor === preset.name}
47
+ <button class="relative size-9 rounded-full border border-border transition-opacity {active ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : 'opacity-70 hover:opacity-100'}" style="background-color: {preset.color}" onclick={() => setColorTheme(preset.name as Parameters<typeof setColorTheme>[0])} aria-label={preset.label} aria-pressed={active}>{#if active}<Check class="absolute inset-0 m-auto size-4 text-white drop-shadow-sm" />{/if}</button>
48
+ {/each}
49
+ </div>
50
+ </SettingsGroup>
51
+ <SettingsGroup title={i18n.t('settings.interface')}>
52
+ <div class="divide-y divide-border">
53
+ <SettingsFieldRow label={i18n.t('settings.language')} description={localeNames[currentLocale] ?? currentLocale}>{#snippet control()}<select class="h-9 max-w-44 rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" value={currentLocale} onchange={(event) => i18n.setLocale((event.target as HTMLSelectElement).value)}>{#each availableLocales as locale (locale)}<option value={locale}>{localeNames[locale] ?? locale}</option>{/each}</select>{/snippet}</SettingsFieldRow>
54
+ <SettingsFieldRow label={i18n.t('settings.sidebarDensity')}>{#snippet control()}<div class="flex overflow-hidden rounded-md border border-input"><button class="px-3 py-1.5 text-xs font-medium {density === 'compact' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}" onclick={() => setDensity('compact')}>{i18n.t('settings.compact')}</button><button class="px-3 py-1.5 text-xs font-medium {density === 'standard' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}" onclick={() => setDensity('standard')}>{i18n.t('settings.standard')}</button></div>{/snippet}</SettingsFieldRow>
55
+ <SettingsFieldRow label={i18n.t('settings.defaultPageSize')}>{#snippet control()}<div class="flex overflow-hidden rounded-md border border-input">{#each pageSizeOptions as size (size)}<button class="px-3 py-1.5 font-mono text-xs font-medium {pageSize === size ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}" onclick={() => setPageSize(size)}>{size}</button>{/each}</div>{/snippet}</SettingsFieldRow>
56
+ </div>
57
+ </SettingsGroup>
187
58
  </div>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import SvadminLogo from './SvadminLogo.svelte';
4
+
5
+ interface Props {
6
+ brand: string;
7
+ title: string;
8
+ description?: string;
9
+ children: Snippet;
10
+ footer?: Snippet;
11
+ }
12
+
13
+ let { brand, title, description, children, footer }: Props = $props();
14
+ </script>
15
+
16
+ <div class="grid min-h-screen bg-muted/20 lg:grid-cols-[minmax(18rem,0.72fr)_minmax(0,1.28fr)]" data-svadmin-auth-page>
17
+ <aside class="hidden border-r border-border bg-card p-10 lg:flex lg:flex-col lg:justify-between xl:p-12">
18
+ <div class="flex items-center gap-3"><SvadminLogo size={40} /><span class="text-lg font-semibold text-foreground">{brand}</span></div>
19
+ <div class="max-w-sm space-y-3"><h2 class="text-2xl font-semibold leading-tight text-foreground">{title}</h2>{#if description}<p class="text-sm leading-6 text-muted-foreground">{description}</p>{/if}</div>
20
+ <p class="text-xs text-muted-foreground">{brand}</p>
21
+ </aside>
22
+ <main class="flex items-center justify-center p-5 sm:p-8 lg:p-12">
23
+ <div class="w-full max-w-md space-y-5">
24
+ <div class="flex items-center gap-3 lg:hidden"><SvadminLogo size={36} /><span class="text-base font-semibold text-foreground">{brand}</span></div>
25
+ <section class="rounded-lg border border-border bg-card p-5 shadow-sm sm:p-7">
26
+ <header class="mb-6 space-y-2"><h1 class="text-xl font-semibold leading-tight text-foreground">{title}</h1>{#if description}<p class="text-sm leading-6 text-muted-foreground">{description}</p>{/if}</header>
27
+ {@render children()}
28
+ </section>
29
+ {#if footer}<div class="text-center">{@render footer()}</div>{/if}
30
+ </div>
31
+ </main>
32
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ brand: string;
4
+ title: string;
5
+ description?: string;
6
+ children: Snippet;
7
+ footer?: Snippet;
8
+ }
9
+ declare const AuthPageShell: import("svelte").Component<Props, {}, "">;
10
+ type AuthPageShell = ReturnType<typeof AuthPageShell>;
11
+ export default AuthPageShell;
@@ -91,11 +91,25 @@
91
91
 
92
92
  // ─── Submission error (non-field, e.g. network error) ─────────────
93
93
  let submitError = $state<string | null>(null);
94
+ let formElement = $state.raw<HTMLFormElement>();
95
+
96
+ function fieldErrorId(fieldKey: string): string {
97
+ return `${resourceName}-${fieldKey}-error`;
98
+ }
99
+
100
+ function focusFirstInvalidField(): void {
101
+ const firstInvalid = formElement?.querySelector<HTMLElement>('[aria-invalid="true"]');
102
+ firstInvalid?.focus();
103
+ }
94
104
 
95
105
  async function handleSubmit() {
96
106
  submitError = null;
97
107
  try {
98
108
  await form.submit();
109
+ if (Object.keys(form.errors).length > 0) {
110
+ queueMicrotask(focusFirstInvalidField);
111
+ return;
112
+ }
99
113
  onSuccess?.();
100
114
  } catch (e) {
101
115
  submitError = e instanceof Error ? e.message : i18n.t('common.operationFailed');
@@ -164,7 +178,7 @@
164
178
  </div>
165
179
  </div>
166
180
  {:else}
167
- <form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-3xl space-y-6">
181
+ <form bind:this={formElement} onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-3xl space-y-6" novalidate>
168
182
  {#if submitError}
169
183
  <div transition:slide={{ duration: 300, axis: 'y' }} class="svadmin-shake">
170
184
  <Alert.Root variant="destructive">
@@ -192,11 +206,13 @@
192
206
  {field}
193
207
  value={form.values[field.key]}
194
208
  onchange={(val: unknown) => form.setFieldValue(field.key, val)}
209
+ invalid={!!form.errors[field.key]}
210
+ errorId={form.errors[field.key] ? fieldErrorId(field.key) : undefined}
195
211
  disabled={isReadonly}
196
212
  />
197
213
  {/if}
198
214
  {#if form.errors[field.key]}
199
- <p class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
215
+ <p id={fieldErrorId(field.key)} class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
200
216
  {/if}
201
217
  </div>
202
218
  {/each}
@@ -215,11 +231,13 @@
215
231
  {field}
216
232
  value={form.values[field.key]}
217
233
  onchange={(val: unknown) => form.setFieldValue(field.key, val)}
234
+ invalid={!!form.errors[field.key]}
235
+ errorId={form.errors[field.key] ? fieldErrorId(field.key) : undefined}
218
236
  disabled={isReadonly}
219
237
  />
220
238
  {/if}
221
239
  {#if form.errors[field.key]}
222
- <p class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
240
+ <p id={fieldErrorId(field.key)} class="text-destructive text-[0.8125rem] mt-1" role="alert" aria-live="polite">{form.errors[field.key]}</p>
223
241
  {/if}
224
242
  </div>
225
243
  {/each}
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { cn, type WithElementRef } from '../utils.js';
5
+ import * as Sheet from './ui/sheet/index.js';
6
+
7
+ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
8
+ open?: boolean;
9
+ title?: string;
10
+ description?: string;
11
+ side?: 'left' | 'right';
12
+ width?: string;
13
+ onClose?: () => void;
14
+ children?: Snippet;
15
+ footer?: Snippet;
16
+ };
17
+
18
+ let {
19
+ open = $bindable(false),
20
+ title = '',
21
+ description = '',
22
+ side = 'right',
23
+ width = 'max-w-md w-full sm:max-w-lg',
24
+ ref = $bindable(null),
25
+ class: className,
26
+ 'aria-label': ariaLabel,
27
+ onClose,
28
+ children,
29
+ footer,
30
+ ...restProps
31
+ }: Props = $props();
32
+
33
+ function handleClose() {
34
+ open = false;
35
+ onClose?.();
36
+ }
37
+ </script>
38
+
39
+ <Sheet.Root
40
+ bind:ref
41
+ bind:open
42
+ {side}
43
+ onClose={handleClose}
44
+ class={cn('overflow-y-auto', width, className)}
45
+ role="dialog"
46
+ aria-modal="true"
47
+ aria-label={ariaLabel || title || 'Details'}
48
+ {...restProps}
49
+ >
50
+ <Sheet.Content class="flex h-full flex-col">
51
+ {#if title || description}
52
+ <Sheet.Header>
53
+ {#if title}
54
+ <Sheet.Title>{title}</Sheet.Title>
55
+ {/if}
56
+ {#if description}
57
+ <Sheet.Description>{description}</Sheet.Description>
58
+ {/if}
59
+ </Sheet.Header>
60
+ {/if}
61
+
62
+ <div class="flex-1 overflow-y-auto py-2">
63
+ {#if children}
64
+ {@render children()}
65
+ {/if}
66
+ </div>
67
+
68
+ {#if footer}
69
+ <Sheet.Footer>
70
+ {@render footer()}
71
+ </Sheet.Footer>
72
+ {/if}
73
+ </Sheet.Content>
74
+ </Sheet.Root>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type WithElementRef } from '../utils.js';
4
+ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
5
+ open?: boolean;
6
+ title?: string;
7
+ description?: string;
8
+ side?: 'left' | 'right';
9
+ width?: string;
10
+ onClose?: () => void;
11
+ children?: Snippet;
12
+ footer?: Snippet;
13
+ };
14
+ declare const DetailDrawer: import("svelte").Component<Props, {}, "open" | "ref">;
15
+ type DetailDrawer = ReturnType<typeof DetailDrawer>;
16
+ export default DetailDrawer;