@noahbaumgartner/svelte-ui 0.0.3 → 0.0.5
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/README.md +69 -38
- package/dist/components/accordion/Accordion.svelte +34 -0
- package/dist/components/accordion/Accordion.svelte.d.ts +11 -0
- package/dist/components/accordion/AccordionItem.svelte +106 -0
- package/dist/components/accordion/AccordionItem.svelte.d.ts +13 -0
- package/dist/components/accordion/context.d.ts +5 -0
- package/dist/components/accordion/context.js +2 -0
- package/dist/components/avatar-group/AvatarGroup.svelte +76 -0
- package/dist/components/avatar-group/AvatarGroup.svelte.d.ts +15 -0
- package/dist/components/drawer/Drawer.svelte +368 -0
- package/dist/components/drawer/Drawer.svelte.d.ts +23 -0
- package/dist/components/empty-state/EmptyState.svelte +86 -0
- package/dist/components/empty-state/EmptyState.svelte.d.ts +16 -0
- package/dist/components/entry/Entry.svelte +83 -0
- package/dist/components/entry/Entry.svelte.d.ts +15 -0
- package/dist/components/footer/Footer.svelte +89 -0
- package/dist/components/footer/Footer.svelte.d.ts +13 -0
- package/dist/components/footer/FooterColumn.svelte +34 -0
- package/dist/components/footer/FooterColumn.svelte.d.ts +10 -0
- package/dist/components/mesh-gradient/MeshGradient.svelte +86 -0
- package/dist/components/mesh-gradient/MeshGradient.svelte.d.ts +12 -0
- package/dist/components/number-stepper/NumberStepper.svelte +82 -0
- package/dist/components/number-stepper/NumberStepper.svelte.d.ts +16 -0
- package/dist/components/preview-card/PreviewCard.svelte +166 -0
- package/dist/components/preview-card/PreviewCard.svelte.d.ts +20 -0
- package/dist/components/progress/Progress.svelte +88 -0
- package/dist/components/progress/Progress.svelte.d.ts +12 -0
- package/dist/components/select/Select.svelte +391 -0
- package/dist/components/select/Select.svelte.d.ts +39 -0
- package/dist/components/sidebar/Sidebar.svelte +37 -0
- package/dist/components/sidebar/SidebarHeader.svelte +26 -2
- package/dist/components/tabs/Tabs.svelte +280 -0
- package/dist/components/tabs/Tabs.svelte.d.ts +48 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type SelectOption<T extends string | number | null = string> = {
|
|
3
|
+
value: T;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" generics="T extends string | number | null">
|
|
10
|
+
import { tick } from 'svelte';
|
|
11
|
+
import { scale } from 'svelte/transition';
|
|
12
|
+
import type { Attachment } from 'svelte/attachments';
|
|
13
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
14
|
+
import { Check, ChevronsUpDown } from '@lucide/svelte';
|
|
15
|
+
|
|
16
|
+
type Props = Omit<
|
|
17
|
+
HTMLButtonAttributes,
|
|
18
|
+
'value' | 'children' | 'onchange' | 'type' | 'role' | 'name'
|
|
19
|
+
> & {
|
|
20
|
+
/** Value of the chosen option. Bindable. */
|
|
21
|
+
value?: T;
|
|
22
|
+
options: SelectOption<T>[];
|
|
23
|
+
/** Shown while no option is chosen. */
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
/** Form field name; submits the value through a hidden input. */
|
|
26
|
+
name?: string;
|
|
27
|
+
onchange?: (value: T) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
value = $bindable(),
|
|
32
|
+
options,
|
|
33
|
+
placeholder = 'Select',
|
|
34
|
+
name,
|
|
35
|
+
disabled = false,
|
|
36
|
+
class: className,
|
|
37
|
+
onchange,
|
|
38
|
+
...rest
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const id = $props.id();
|
|
42
|
+
const gap = 4;
|
|
43
|
+
const edge = 8;
|
|
44
|
+
const maxHeight = 320;
|
|
45
|
+
|
|
46
|
+
let open = $state(false);
|
|
47
|
+
let wrapperEl: HTMLSpanElement;
|
|
48
|
+
let triggerEl: HTMLButtonElement;
|
|
49
|
+
let listEl: HTMLDivElement | undefined = $state();
|
|
50
|
+
let placement: 'top' | 'bottom' = $state('bottom');
|
|
51
|
+
let position: { top?: number; bottom?: number; left: number; width: number; height: number } =
|
|
52
|
+
$state({ left: 0, width: 0, height: maxHeight });
|
|
53
|
+
|
|
54
|
+
let current = $derived(options.find((option) => option.value === value));
|
|
55
|
+
|
|
56
|
+
function items() {
|
|
57
|
+
return Array.from(
|
|
58
|
+
listEl?.querySelectorAll<HTMLElement>('[role="option"]:not(:disabled)') ?? []
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function show(focus: 'first' | 'last' = 'first') {
|
|
63
|
+
open = true;
|
|
64
|
+
await tick();
|
|
65
|
+
const list = items();
|
|
66
|
+
const selected = list.find((item) => item.getAttribute('aria-selected') === 'true');
|
|
67
|
+
(selected ?? (focus === 'first' ? list[0] : list.at(-1)))?.focus();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function close({ restoreFocus = false } = {}) {
|
|
71
|
+
open = false;
|
|
72
|
+
if (restoreFocus) triggerEl.focus();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function choose(option: SelectOption<T>) {
|
|
76
|
+
value = option.value;
|
|
77
|
+
onchange?.(option.value);
|
|
78
|
+
close({ restoreFocus: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The list sits in the top layer so dialogs and scroll containers cannot clip it */
|
|
82
|
+
function place() {
|
|
83
|
+
if (!listEl) return;
|
|
84
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
85
|
+
const below = window.innerHeight - rect.bottom - gap - edge;
|
|
86
|
+
const above = rect.top - gap - edge;
|
|
87
|
+
const wanted = Math.min(listEl.scrollHeight + 2, maxHeight);
|
|
88
|
+
placement = below < wanted && above > below ? 'top' : 'bottom';
|
|
89
|
+
const left = Math.max(edge, Math.min(rect.left, window.innerWidth - listEl.offsetWidth - edge));
|
|
90
|
+
position =
|
|
91
|
+
placement === 'top'
|
|
92
|
+
? {
|
|
93
|
+
bottom: window.innerHeight - rect.top + gap,
|
|
94
|
+
left,
|
|
95
|
+
width: rect.width,
|
|
96
|
+
height: Math.min(maxHeight, above)
|
|
97
|
+
}
|
|
98
|
+
: { top: rect.bottom + gap, left, width: rect.width, height: Math.min(maxHeight, below) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const popover: Attachment<HTMLDivElement> = (node) => {
|
|
102
|
+
node.showPopover();
|
|
103
|
+
place();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/* Typing jumps to the option that starts with the typed letters */
|
|
107
|
+
let typed = '';
|
|
108
|
+
let typedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
109
|
+
|
|
110
|
+
function typeahead(key: string) {
|
|
111
|
+
clearTimeout(typedTimer);
|
|
112
|
+
typed += key.toLowerCase();
|
|
113
|
+
typedTimer = setTimeout(() => (typed = ''), 500);
|
|
114
|
+
items()
|
|
115
|
+
.find((item) => item.textContent?.trim().toLowerCase().startsWith(typed))
|
|
116
|
+
?.focus();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function handleListKeydown(event: KeyboardEvent) {
|
|
120
|
+
if (event.key === 'Escape') {
|
|
121
|
+
// Only the list closes, not a Dialog or Drawer around it
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
close({ restoreFocus: true });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (event.key === 'Tab') {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
close({ restoreFocus: true });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const list = items();
|
|
134
|
+
const index = list.indexOf(document.activeElement as HTMLElement);
|
|
135
|
+
const last = list.length - 1;
|
|
136
|
+
const target = {
|
|
137
|
+
ArrowDown: index === last ? 0 : index + 1,
|
|
138
|
+
ArrowUp: index <= 0 ? last : index - 1,
|
|
139
|
+
Home: 0,
|
|
140
|
+
End: last
|
|
141
|
+
}[event.key];
|
|
142
|
+
|
|
143
|
+
if (target !== undefined) {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
list[target]?.focus();
|
|
146
|
+
} else if (event.key.length === 1 && event.key !== ' ' && !event.metaKey && !event.ctrlKey) {
|
|
147
|
+
typeahead(event.key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$effect(() => {
|
|
152
|
+
if (!open) return;
|
|
153
|
+
|
|
154
|
+
function handlePointerDown(event: PointerEvent) {
|
|
155
|
+
if (!wrapperEl.contains(event.target as Node)) close();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
window.addEventListener('pointerdown', handlePointerDown);
|
|
159
|
+
window.addEventListener('scroll', place, true);
|
|
160
|
+
window.addEventListener('resize', place);
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
window.removeEventListener('pointerdown', handlePointerDown);
|
|
164
|
+
window.removeEventListener('scroll', place, true);
|
|
165
|
+
window.removeEventListener('resize', place);
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<span class={['select', className]} bind:this={wrapperEl}>
|
|
171
|
+
<button
|
|
172
|
+
{...rest}
|
|
173
|
+
bind:this={triggerEl}
|
|
174
|
+
type="button"
|
|
175
|
+
role="combobox"
|
|
176
|
+
aria-haspopup="listbox"
|
|
177
|
+
aria-expanded={open}
|
|
178
|
+
aria-controls="{id}-listbox"
|
|
179
|
+
{disabled}
|
|
180
|
+
class="select-trigger"
|
|
181
|
+
onclick={(event) => {
|
|
182
|
+
rest.onclick?.(event);
|
|
183
|
+
if (open) close();
|
|
184
|
+
else show();
|
|
185
|
+
}}
|
|
186
|
+
onkeydown={(event) => {
|
|
187
|
+
rest.onkeydown?.(event);
|
|
188
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
show(event.key === 'ArrowDown' ? 'first' : 'last');
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<span class={['select-value', { 'select-value--placeholder': !current }]}>
|
|
194
|
+
{current?.label ?? placeholder}
|
|
195
|
+
</span>
|
|
196
|
+
<ChevronsUpDown class="select-chevron" aria-hidden="true" />
|
|
197
|
+
</button>
|
|
198
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
199
|
+
|
|
200
|
+
{#if open}
|
|
201
|
+
<div
|
|
202
|
+
id="{id}-listbox"
|
|
203
|
+
role="listbox"
|
|
204
|
+
tabindex="-1"
|
|
205
|
+
popover="manual"
|
|
206
|
+
class={['select-list', `select-list--${placement}`]}
|
|
207
|
+
style:top={position.top === undefined ? undefined : `${position.top}px`}
|
|
208
|
+
style:bottom={position.bottom === undefined ? undefined : `${position.bottom}px`}
|
|
209
|
+
style:left="{position.left}px"
|
|
210
|
+
style:min-width="{position.width}px"
|
|
211
|
+
style:max-height="{position.height}px"
|
|
212
|
+
bind:this={listEl}
|
|
213
|
+
onkeydown={handleListKeydown}
|
|
214
|
+
onfocusout={(event) => {
|
|
215
|
+
if (!wrapperEl.contains(event.relatedTarget as Node | null)) close();
|
|
216
|
+
}}
|
|
217
|
+
{@attach popover}
|
|
218
|
+
transition:scale={{ duration: 140, start: 0.95 }}
|
|
219
|
+
>
|
|
220
|
+
{#each options as option (option.value)}
|
|
221
|
+
{@const selected = option.value === value}
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
role="option"
|
|
225
|
+
tabindex="-1"
|
|
226
|
+
aria-selected={selected}
|
|
227
|
+
disabled={option.disabled}
|
|
228
|
+
class={['select-option', { 'select-option--selected': selected }]}
|
|
229
|
+
onclick={() => choose(option)}
|
|
230
|
+
>
|
|
231
|
+
<span class="select-option-label">{option.label}</span>
|
|
232
|
+
<span class="select-option-check">
|
|
233
|
+
{#if selected}<Check aria-hidden="true" />{/if}
|
|
234
|
+
</span>
|
|
235
|
+
</button>
|
|
236
|
+
{/each}
|
|
237
|
+
</div>
|
|
238
|
+
{/if}
|
|
239
|
+
</span>
|
|
240
|
+
|
|
241
|
+
<style>
|
|
242
|
+
.select {
|
|
243
|
+
display: flex;
|
|
244
|
+
width: 100%;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Looks like an Input */
|
|
249
|
+
.select-trigger {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: space-between;
|
|
253
|
+
gap: 8px;
|
|
254
|
+
box-sizing: border-box;
|
|
255
|
+
width: 100%;
|
|
256
|
+
min-width: 0;
|
|
257
|
+
height: 32px;
|
|
258
|
+
padding: 0 8px 0 10px;
|
|
259
|
+
font-family: inherit;
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
color: var(--color-text);
|
|
262
|
+
text-align: left;
|
|
263
|
+
background-color: transparent;
|
|
264
|
+
border: 1px solid var(--color-border-strong);
|
|
265
|
+
border-radius: 10px;
|
|
266
|
+
outline: none;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
transition: border-color 200ms ease;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.select-trigger:focus-visible,
|
|
272
|
+
.select-trigger[aria-expanded='true'] {
|
|
273
|
+
border-color: var(--color-accent);
|
|
274
|
+
outline: 1px solid var(--color-accent);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.select-trigger[aria-invalid='true'] {
|
|
278
|
+
border-color: var(--color-destructive);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.select-trigger[aria-invalid='true']:is(:focus-visible, [aria-expanded='true']) {
|
|
282
|
+
outline-color: var(--color-destructive);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.select-trigger:disabled {
|
|
286
|
+
cursor: not-allowed;
|
|
287
|
+
opacity: 0.5;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.select-value {
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
text-overflow: ellipsis;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.select-value--placeholder {
|
|
297
|
+
color: var(--color-text-faint);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.select-trigger :global(.select-chevron) {
|
|
301
|
+
width: 14px;
|
|
302
|
+
height: 14px;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
color: var(--color-text-muted);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.select-list {
|
|
308
|
+
position: fixed;
|
|
309
|
+
inset: auto;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
gap: 2px;
|
|
312
|
+
box-sizing: border-box;
|
|
313
|
+
max-width: calc(100vw - 16px);
|
|
314
|
+
margin: 0;
|
|
315
|
+
padding: 4px;
|
|
316
|
+
overflow-y: auto;
|
|
317
|
+
overscroll-behavior: contain;
|
|
318
|
+
background-color: var(--color-bg);
|
|
319
|
+
color: var(--color-text);
|
|
320
|
+
border: 1px solid var(--color-border);
|
|
321
|
+
border-radius: 10px;
|
|
322
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
323
|
+
outline: none;
|
|
324
|
+
user-select: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.select-list:popover-open {
|
|
328
|
+
display: flex;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.select-list--bottom {
|
|
332
|
+
transform-origin: top left;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.select-list--top {
|
|
336
|
+
transform-origin: bottom left;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.select-option {
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-shrink: 0;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 10px;
|
|
344
|
+
width: 100%;
|
|
345
|
+
padding: 8px 10px;
|
|
346
|
+
font: inherit;
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
color: var(--color-text);
|
|
349
|
+
text-align: left;
|
|
350
|
+
background-color: transparent;
|
|
351
|
+
border: none;
|
|
352
|
+
border-radius: 6px;
|
|
353
|
+
outline: none;
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: background-color 150ms ease;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* Plain :focus, so the current option also shows after opening with the mouse */
|
|
359
|
+
.select-option:hover:not(:disabled),
|
|
360
|
+
.select-option:focus {
|
|
361
|
+
background-color: var(--color-surface);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.select-option:disabled {
|
|
365
|
+
cursor: not-allowed;
|
|
366
|
+
opacity: 0.5;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.select-option--selected {
|
|
370
|
+
font-weight: 600;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.select-option-label {
|
|
374
|
+
flex-grow: 1;
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
text-overflow: ellipsis;
|
|
377
|
+
white-space: nowrap;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.select-option-check {
|
|
381
|
+
display: flex;
|
|
382
|
+
width: 14px;
|
|
383
|
+
height: 14px;
|
|
384
|
+
flex-shrink: 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.select-option-check :global(svg) {
|
|
388
|
+
width: 14px;
|
|
389
|
+
height: 14px;
|
|
390
|
+
}
|
|
391
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SelectOption<T extends string | number | null = string> = {
|
|
2
|
+
value: T;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
7
|
+
declare function $$render<T extends string | number | null>(): {
|
|
8
|
+
props: Omit<HTMLButtonAttributes, "children" | "role" | "onchange" | "name" | "type" | "value"> & {
|
|
9
|
+
/** Value of the chosen option. Bindable. */
|
|
10
|
+
value?: T;
|
|
11
|
+
options: SelectOption<T>[];
|
|
12
|
+
/** Shown while no option is chosen. */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Form field name; submits the value through a hidden input. */
|
|
15
|
+
name?: string;
|
|
16
|
+
onchange?: (value: T) => void;
|
|
17
|
+
};
|
|
18
|
+
exports: {};
|
|
19
|
+
bindings: "value";
|
|
20
|
+
slots: {};
|
|
21
|
+
events: {};
|
|
22
|
+
};
|
|
23
|
+
declare class __sveltets_Render<T extends string | number | null> {
|
|
24
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
25
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
26
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
27
|
+
bindings(): "value";
|
|
28
|
+
exports(): {};
|
|
29
|
+
}
|
|
30
|
+
interface $$IsomorphicComponent {
|
|
31
|
+
new <T extends string | number | null>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
32
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
33
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
<T extends string | number | null>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
35
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
36
|
+
}
|
|
37
|
+
declare const Select: $$IsomorphicComponent;
|
|
38
|
+
type Select<T extends string | number | null> = InstanceType<typeof Select<T>>;
|
|
39
|
+
export default Select;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { fade } from 'svelte/transition';
|
|
5
|
+
import X from '@lucide/svelte/icons/x';
|
|
6
|
+
import Button from '../button/Button.svelte';
|
|
5
7
|
import { getSidebarContext } from './context.js';
|
|
6
8
|
|
|
7
9
|
type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
@@ -70,6 +72,17 @@
|
|
|
70
72
|
]}
|
|
71
73
|
>
|
|
72
74
|
<div class="sidebar-inner">
|
|
75
|
+
{#if sidebar.mobile}
|
|
76
|
+
<div class="sidebar-close">
|
|
77
|
+
<Button
|
|
78
|
+
variant="ghost"
|
|
79
|
+
size="sm"
|
|
80
|
+
icon={X}
|
|
81
|
+
label="Close sidebar"
|
|
82
|
+
onclick={sidebar.closeMobile}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
73
86
|
{@render children()}
|
|
74
87
|
</div>
|
|
75
88
|
</aside>
|
|
@@ -154,11 +167,24 @@
|
|
|
154
167
|
bottom: 0;
|
|
155
168
|
left: 0;
|
|
156
169
|
z-index: 50;
|
|
170
|
+
width: 100%;
|
|
157
171
|
height: auto;
|
|
158
172
|
transform: translateX(-100%);
|
|
159
173
|
transition: transform 200ms ease;
|
|
160
174
|
}
|
|
161
175
|
|
|
176
|
+
.sidebar--mobile .sidebar-inner {
|
|
177
|
+
width: 100%;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.sidebar--mobile.sidebar--sidebar .sidebar-inner {
|
|
181
|
+
border-inline: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.sidebar--mobile.sidebar--floating .sidebar-inner {
|
|
185
|
+
width: calc(100% - 16px);
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
.sidebar--mobile.sidebar--right {
|
|
163
189
|
left: auto;
|
|
164
190
|
right: 0;
|
|
@@ -169,6 +195,17 @@
|
|
|
169
195
|
transform: none;
|
|
170
196
|
}
|
|
171
197
|
|
|
198
|
+
/* Fallback for sidebars without a `SidebarHeader`, which otherwise holds the close button */
|
|
199
|
+
.sidebar-close {
|
|
200
|
+
display: flex;
|
|
201
|
+
justify-content: flex-end;
|
|
202
|
+
padding: 8px 8px 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.sidebar-inner:has(:global(.sidebar-header)) .sidebar-close {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
172
209
|
.sidebar-backdrop {
|
|
173
210
|
position: fixed;
|
|
174
211
|
inset: 0;
|
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import X from '@lucide/svelte/icons/x';
|
|
5
|
+
import Button from '../button/Button.svelte';
|
|
6
|
+
import { getSidebarContext } from './context.js';
|
|
4
7
|
|
|
5
8
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
6
9
|
children: Snippet;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
let { class: className, children, ...rest }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const sidebar = getSidebarContext();
|
|
10
15
|
</script>
|
|
11
16
|
|
|
12
17
|
<div {...rest} class={['sidebar-header', className]}>
|
|
13
|
-
|
|
18
|
+
<div class="sidebar-header-content">
|
|
19
|
+
{@render children()}
|
|
20
|
+
</div>
|
|
21
|
+
{#if sidebar.mobile}
|
|
22
|
+
<Button
|
|
23
|
+
variant="ghost"
|
|
24
|
+
size="sm"
|
|
25
|
+
icon={X}
|
|
26
|
+
label="Close sidebar"
|
|
27
|
+
onclick={sidebar.closeMobile}
|
|
28
|
+
/>
|
|
29
|
+
{/if}
|
|
14
30
|
</div>
|
|
15
31
|
|
|
16
32
|
<style>
|
|
17
33
|
.sidebar-header {
|
|
18
34
|
display: flex;
|
|
19
|
-
|
|
35
|
+
align-items: center;
|
|
20
36
|
gap: 8px;
|
|
21
37
|
padding: 8px;
|
|
22
38
|
}
|
|
39
|
+
|
|
40
|
+
.sidebar-header-content {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex: 1;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
23
47
|
</style>
|