@svadmin/ui 0.46.0 → 0.48.0
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/AboutSettings.svelte +21 -96
- package/dist/components/AdminApp.svelte +20 -0
- package/dist/components/AdminApp.svelte.d.ts +5 -1
- package/dist/components/ApiSettings.svelte +153 -246
- package/dist/components/AppearanceSettings.svelte +36 -165
- package/dist/components/AutoForm.svelte +21 -3
- package/dist/components/DetailDrawer.svelte +74 -0
- package/dist/components/DetailDrawer.svelte.d.ts +16 -0
- package/dist/components/FieldRenderer.svelte +70 -2
- package/dist/components/FieldRenderer.svelte.d.ts +2 -0
- package/dist/components/IntegrationsSettings.svelte +22 -61
- package/dist/components/Layout.svelte +15 -1
- package/dist/components/NotificationsSettings.svelte +22 -103
- package/dist/components/PasswordInput.svelte +4 -0
- package/dist/components/PasswordInput.svelte.d.ts +1 -0
- package/dist/components/PermissionMatrix.svelte +17 -8
- package/dist/components/RowActions.svelte +154 -0
- package/dist/components/RowActions.svelte.d.ts +26 -0
- package/dist/components/SecuritySettings.svelte +165 -140
- package/dist/components/SecuritySettings.svelte.d.ts +1 -2
- package/dist/components/SettingsPage.svelte +12 -2
- package/dist/components/account/CompanyProfilePage.svelte +10 -4
- package/dist/components/account/SettingsEnterprisePage.svelte +227 -23
- package/dist/components/account/SettingsSidebarPage.svelte +12 -1
- package/dist/components/account/UserProfilePage.svelte +5 -4
- package/dist/components/content/ApiKeyList.svelte +30 -5
- package/dist/components/content/ApiKeyList.svelte.d.ts +3 -0
- package/dist/components/content/WorkspaceLayout.svelte +7 -4
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +1 -0
- package/dist/components/ui/sheet/sheet.svelte +122 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +5 -2
|
@@ -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
|
|
6
|
-
import
|
|
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
|
|
41
|
-
|
|
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
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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>
|
|
@@ -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;
|