@noahbaumgartner/svelte-ui 0.0.4 → 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.
Files changed (36) hide show
  1. package/README.md +8 -8
  2. package/dist/components/accordion/Accordion.svelte +34 -0
  3. package/dist/components/accordion/Accordion.svelte.d.ts +11 -0
  4. package/dist/components/accordion/AccordionItem.svelte +106 -0
  5. package/dist/components/accordion/AccordionItem.svelte.d.ts +13 -0
  6. package/dist/components/accordion/context.d.ts +5 -0
  7. package/dist/components/accordion/context.js +2 -0
  8. package/dist/components/avatar-group/AvatarGroup.svelte +76 -0
  9. package/dist/components/avatar-group/AvatarGroup.svelte.d.ts +15 -0
  10. package/dist/components/drawer/Drawer.svelte +368 -0
  11. package/dist/components/drawer/Drawer.svelte.d.ts +23 -0
  12. package/dist/components/empty-state/EmptyState.svelte +86 -0
  13. package/dist/components/empty-state/EmptyState.svelte.d.ts +16 -0
  14. package/dist/components/entry/Entry.svelte +83 -0
  15. package/dist/components/entry/Entry.svelte.d.ts +15 -0
  16. package/dist/components/footer/Footer.svelte +89 -0
  17. package/dist/components/footer/Footer.svelte.d.ts +13 -0
  18. package/dist/components/footer/FooterColumn.svelte +34 -0
  19. package/dist/components/footer/FooterColumn.svelte.d.ts +10 -0
  20. package/dist/components/mesh-gradient/MeshGradient.svelte +86 -0
  21. package/dist/components/mesh-gradient/MeshGradient.svelte.d.ts +12 -0
  22. package/dist/components/number-stepper/NumberStepper.svelte +82 -0
  23. package/dist/components/number-stepper/NumberStepper.svelte.d.ts +16 -0
  24. package/dist/components/preview-card/PreviewCard.svelte +166 -0
  25. package/dist/components/preview-card/PreviewCard.svelte.d.ts +20 -0
  26. package/dist/components/progress/Progress.svelte +88 -0
  27. package/dist/components/progress/Progress.svelte.d.ts +12 -0
  28. package/dist/components/select/Select.svelte +391 -0
  29. package/dist/components/select/Select.svelte.d.ts +39 -0
  30. package/dist/components/sidebar/Sidebar.svelte +37 -0
  31. package/dist/components/sidebar/SidebarHeader.svelte +26 -2
  32. package/dist/components/tabs/Tabs.svelte +280 -0
  33. package/dist/components/tabs/Tabs.svelte.d.ts +48 -0
  34. package/dist/index.d.ts +15 -0
  35. package/dist/index.js +14 -0
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -62,14 +62,14 @@ See [`src/lib/styles/tokens.css`](src/lib/styles/tokens.css) for every token.
62
62
 
63
63
  ## Components
64
64
 
65
- | Category | Components |
66
- | ---------- | ---------------------------------------------------------------------------------------------- |
67
- | Actions | `Button`, `ButtonGroup`, `Toggle`, `Link` |
68
- | Forms | `Input`, `InputGroup`, `Textarea`, `Checkbox`, `RadioGroup`, `Switch`, `Label`, `Field` family |
69
- | Pickers | `Calendar`, `TimePicker`, `ColorPicker` |
70
- | Overlays | `Dialog`, `AlertDialog`, `Popover`, `Tooltip`, `ContextMenu`, `Toaster` + `toast()` |
71
- | Navigation | `Navbar`, `NavigationMenu`, `Sidebar` family, `Breadcrumb` |
72
- | Display | `Card` family, `Table`, `Avatar`, `Badge`, `Kbd`, `Separator`, `Skeleton`, `Spinner` |
65
+ | Category | Components |
66
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
67
+ | Actions | `Button`, `ButtonGroup`, `Toggle`, `Link` |
68
+ | Forms | `Input`, `InputGroup`, `Textarea`, `Select`, `NumberStepper`, `Checkbox`, `RadioGroup`, `Switch`, `Label`, `Field` family |
69
+ | Pickers | `Calendar`, `TimePicker`, `ColorPicker` |
70
+ | Overlays | `Dialog`, `AlertDialog`, `Drawer`, `Popover`, `Tooltip`, `ContextMenu`, `Toaster` + `toast()` |
71
+ | Navigation | `Navbar`, `NavigationMenu`, `Sidebar` family, `Breadcrumb`, `Tabs` |
72
+ | Display | `Card` family, `Table`, `Accordion`, `Avatar`, `AvatarGroup`, `Badge`, `EmptyState`, `Kbd`, `Progress`, `Separator`, `Skeleton`, `Spinner` |
73
73
 
74
74
  Every component is documented in Storybook, with one story per prop.
75
75
 
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { setAccordionContext } from './context.js';
5
+
6
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
7
+ /** Lets several items be open at once. By default opening one closes the others. */
8
+ multiple?: boolean;
9
+ /** AccordionItems. */
10
+ children: Snippet;
11
+ };
12
+
13
+ let { multiple = false, class: className, children, ...rest }: Props = $props();
14
+
15
+ const name = $props.id();
16
+
17
+ setAccordionContext({
18
+ get name() {
19
+ return multiple ? undefined : name;
20
+ }
21
+ });
22
+ </script>
23
+
24
+ <div {...rest} class={['accordion', className]}>
25
+ {@render children()}
26
+ </div>
27
+
28
+ <style>
29
+ .accordion {
30
+ display: flex;
31
+ flex-direction: column;
32
+ width: 100%;
33
+ }
34
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
+ /** Lets several items be open at once. By default opening one closes the others. */
5
+ multiple?: boolean;
6
+ /** AccordionItems. */
7
+ children: Snippet;
8
+ };
9
+ declare const Accordion: import("svelte").Component<Props, {}, "">;
10
+ type Accordion = ReturnType<typeof Accordion>;
11
+ export default Accordion;
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLDetailsAttributes } from 'svelte/elements';
4
+ import { ChevronDown } from '@lucide/svelte';
5
+ import { getAccordionContext } from './context.js';
6
+
7
+ type Props = Omit<HTMLDetailsAttributes, 'children' | 'title' | 'name'> & {
8
+ /** Whether the item is expanded. Bindable. */
9
+ open?: boolean;
10
+ /** The always visible heading that toggles the item. */
11
+ title: string;
12
+ /** The content shown while the item is open. */
13
+ children: Snippet;
14
+ };
15
+
16
+ let { open = $bindable(false), title, class: className, children, ...rest }: Props = $props();
17
+
18
+ const accordion = getAccordionContext();
19
+ </script>
20
+
21
+ <details {...rest} bind:open name={accordion.name} class={['accordion-item', className]}>
22
+ <summary class="accordion-trigger">
23
+ <span class="accordion-title">{title}</span>
24
+ <ChevronDown class="accordion-chevron" aria-hidden="true" />
25
+ </summary>
26
+ <div class="accordion-content">{@render children()}</div>
27
+ </details>
28
+
29
+ <style>
30
+ .accordion-item {
31
+ font-size: 13px;
32
+ color: var(--color-text);
33
+ border-bottom: 1px solid var(--color-border);
34
+ /* Lets the height animate to and from `auto` where the browser supports it */
35
+ interpolate-size: allow-keywords;
36
+ }
37
+
38
+ .accordion-item:last-child {
39
+ border-bottom: 0;
40
+ }
41
+
42
+ .accordion-trigger {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ gap: 12px;
47
+ padding: 14px 0;
48
+ font-weight: 500;
49
+ list-style: none;
50
+ border-radius: 6px;
51
+ outline: none;
52
+ cursor: pointer;
53
+ user-select: none;
54
+ }
55
+
56
+ .accordion-trigger::-webkit-details-marker {
57
+ display: none;
58
+ }
59
+
60
+ .accordion-trigger:hover .accordion-title {
61
+ text-decoration: underline;
62
+ text-underline-offset: 3px;
63
+ }
64
+
65
+ .accordion-trigger:focus-visible {
66
+ outline: 2px solid var(--color-accent);
67
+ outline-offset: 2px;
68
+ }
69
+
70
+ .accordion-trigger :global(.accordion-chevron) {
71
+ width: 16px;
72
+ height: 16px;
73
+ flex-shrink: 0;
74
+ color: var(--color-text-muted);
75
+ transition: rotate 200ms ease;
76
+ }
77
+
78
+ .accordion-item[open] > .accordion-trigger :global(.accordion-chevron) {
79
+ rotate: 180deg;
80
+ }
81
+
82
+ .accordion-item::details-content {
83
+ block-size: 0;
84
+ overflow: clip;
85
+ transition:
86
+ block-size 200ms ease,
87
+ content-visibility 200ms allow-discrete;
88
+ }
89
+
90
+ .accordion-item[open]::details-content {
91
+ block-size: auto;
92
+ }
93
+
94
+ .accordion-content {
95
+ padding-bottom: 14px;
96
+ line-height: 1.5;
97
+ color: var(--color-text-secondary);
98
+ }
99
+
100
+ @media (prefers-reduced-motion: reduce) {
101
+ .accordion-item::details-content,
102
+ .accordion-trigger :global(.accordion-chevron) {
103
+ transition: none;
104
+ }
105
+ }
106
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLDetailsAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLDetailsAttributes, 'children' | 'title' | 'name'> & {
4
+ /** Whether the item is expanded. Bindable. */
5
+ open?: boolean;
6
+ /** The always visible heading that toggles the item. */
7
+ title: string;
8
+ /** The content shown while the item is open. */
9
+ children: Snippet;
10
+ };
11
+ declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
12
+ type AccordionItem = ReturnType<typeof AccordionItem>;
13
+ export default AccordionItem;
@@ -0,0 +1,5 @@
1
+ export type AccordionContext = {
2
+ /** Shared `name` of the items' <details>, so the browser keeps only one open. Undefined with `multiple`. */
3
+ readonly name: string | undefined;
4
+ };
5
+ export declare const getAccordionContext: () => AccordionContext, setAccordionContext: (context: AccordionContext) => AccordionContext;
@@ -0,0 +1,2 @@
1
+ import { createContext } from 'svelte';
2
+ export const [getAccordionContext, setAccordionContext] = createContext();
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
6
+ /** Accessible name for the group, e.g. "Members". */
7
+ label?: string;
8
+ /** How many more there are than the Avatars shown; rendered as "+3" at the end. */
9
+ more?: number;
10
+ /** Size of the "+3"; match it to the Avatars' `size`. */
11
+ size?: 'sm' | 'md' | 'lg';
12
+ /** Avatars, overlapping each other. */
13
+ children: Snippet;
14
+ };
15
+
16
+ let { label, more = 0, size = 'md', class: className, children, ...rest }: Props = $props();
17
+ </script>
18
+
19
+ <div {...rest} role="group" aria-label={label} class={['avatar-group', className]}>
20
+ {@render children()}
21
+ {#if more > 0}
22
+ <span class={['avatar-group-more', `avatar-group-more--${size}`]}>+{more}</span>
23
+ {/if}
24
+ </div>
25
+
26
+ <style>
27
+ .avatar-group {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ }
31
+
32
+ /* The ring in the page colour separates the overlapping avatars */
33
+ .avatar-group > :global(*) {
34
+ box-shadow: 0 0 0 2px var(--color-bg);
35
+ }
36
+
37
+ .avatar-group > :global(:not(:first-child)) {
38
+ margin-inline-start: -6px;
39
+ }
40
+
41
+ .avatar-group-more {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ box-sizing: border-box;
46
+ flex-shrink: 0;
47
+ padding: 0 6px;
48
+ font-weight: 500;
49
+ font-variant-numeric: tabular-nums;
50
+ color: var(--color-text-secondary);
51
+ background-color: var(--color-surface);
52
+ user-select: none;
53
+ }
54
+
55
+ /* Same sizes and corners as Avatar */
56
+ .avatar-group-more--sm {
57
+ min-width: 24px;
58
+ height: 24px;
59
+ border-radius: 8px;
60
+ font-size: 10px;
61
+ }
62
+
63
+ .avatar-group-more--md {
64
+ min-width: 32px;
65
+ height: 32px;
66
+ border-radius: 10px;
67
+ font-size: 12px;
68
+ }
69
+
70
+ .avatar-group-more--lg {
71
+ min-width: 40px;
72
+ height: 40px;
73
+ border-radius: 12px;
74
+ font-size: 14px;
75
+ }
76
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
+ /** Accessible name for the group, e.g. "Members". */
5
+ label?: string;
6
+ /** How many more there are than the Avatars shown; rendered as "+3" at the end. */
7
+ more?: number;
8
+ /** Size of the "+3"; match it to the Avatars' `size`. */
9
+ size?: 'sm' | 'md' | 'lg';
10
+ /** Avatars, overlapping each other. */
11
+ children: Snippet;
12
+ };
13
+ declare const AvatarGroup: import("svelte").Component<Props, {}, "">;
14
+ type AvatarGroup = ReturnType<typeof AvatarGroup>;
15
+ export default AvatarGroup;
@@ -0,0 +1,368 @@
1
+ <script module lang="ts">
2
+ export type DrawerTriggerProps = {
3
+ onclick: () => void;
4
+ 'aria-haspopup': 'dialog';
5
+ };
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import type { Snippet } from 'svelte';
10
+ import { X } from '@lucide/svelte';
11
+
12
+ type Props = {
13
+ /**
14
+ * Whether the drawer is open. Bindable. A sheet from the bottom on phones, a panel on the right from 768px up.
15
+ * Escape, the handle or close button, dragging it away and a click on the backdrop close it.
16
+ */
17
+ open?: boolean;
18
+ title: string;
19
+ description?: string;
20
+ /** Renders the trigger. Spread the given props onto it, e.g. `<Button {...props} />`. Omit to control `open` yourself. */
21
+ trigger?: Snippet<[DrawerTriggerProps]>;
22
+ /** The drawer's content; scrolls when it is taller than the screen. The first focusable element in it gets the focus on open. */
23
+ children?: Snippet;
24
+ /** Optional Buttons in the footer, stacked at full width with the primary one last; it ends up on top. Call `close` to close the drawer. */
25
+ actions?: Snippet<[close: () => void]>;
26
+ };
27
+
28
+ let { open = $bindable(false), title, description, trigger, children, actions }: Props = $props();
29
+
30
+ const id = $props.id();
31
+ let dialogEl: HTMLDialogElement;
32
+
33
+ const triggerProps: DrawerTriggerProps = {
34
+ onclick: () => (open = true),
35
+ 'aria-haspopup': 'dialog'
36
+ };
37
+
38
+ function close() {
39
+ open = false;
40
+ }
41
+
42
+ $effect(() => {
43
+ if (open && !dialogEl.open) {
44
+ dialogEl.showModal();
45
+ } else if (!open && dialogEl.open) {
46
+ dialogEl.close();
47
+ }
48
+ });
49
+
50
+ /* Dragging the header towards the edge moves the drawer along; letting go far enough or fast enough closes it */
51
+ const slop = 4;
52
+ let offset = $state(0);
53
+ let dragging = $state(false);
54
+ /* Matches the media query below: the panel on the right leaves sideways */
55
+ let sideways = $state(false);
56
+ let dragged = false;
57
+
58
+ function startDrag(event: PointerEvent) {
59
+ if (event.button !== 0) return;
60
+ sideways = window.matchMedia('(min-width: 768px)').matches;
61
+ const start = sideways ? event.clientX : event.clientY;
62
+ const startTime = event.timeStamp;
63
+
64
+ function move(e: PointerEvent) {
65
+ const distance = (sideways ? e.clientX : e.clientY) - start;
66
+ if (!dragging && Math.abs(distance) < slop) return;
67
+ dragging = true;
68
+ dragged = true;
69
+ offset = Math.max(0, distance);
70
+ }
71
+
72
+ function end(e: PointerEvent) {
73
+ window.removeEventListener('pointermove', move);
74
+ window.removeEventListener('pointerup', end);
75
+ window.removeEventListener('pointercancel', end);
76
+ if (!dragging) return;
77
+ const velocity = offset / Math.max(1, e.timeStamp - startTime);
78
+ const dismiss =
79
+ e.type === 'pointerup' &&
80
+ (offset > (sideways ? dialogEl.offsetWidth : dialogEl.offsetHeight) / 3 || velocity > 0.5);
81
+ dragging = false;
82
+ offset = 0;
83
+ if (dismiss) close();
84
+ // The click that follows a drag ending on the handle must not count as a tap on it
85
+ setTimeout(() => (dragged = false));
86
+ }
87
+
88
+ window.addEventListener('pointermove', move);
89
+ window.addEventListener('pointerup', end);
90
+ window.addEventListener('pointercancel', end);
91
+ }
92
+ </script>
93
+
94
+ {@render trigger?.(triggerProps)}
95
+
96
+ <dialog
97
+ bind:this={dialogEl}
98
+ aria-labelledby="{id}-title"
99
+ aria-describedby={description ? `${id}-description` : undefined}
100
+ class={['drawer', { 'drawer--dragging': dragging }]}
101
+ style:translate={dragging ? (sideways ? `${offset}px 0` : `0 ${offset}px`) : undefined}
102
+ onkeydown={(event) => {
103
+ if (event.key !== 'Escape') return;
104
+ event.preventDefault();
105
+ close();
106
+ }}
107
+ oncancel={(event) => {
108
+ event.preventDefault();
109
+ close();
110
+ }}
111
+ onclose={() => (open = false)}
112
+ onclick={(event) => {
113
+ // Clicks on the ::backdrop target the <dialog> itself, outside its box
114
+ if (event.target !== dialogEl) return;
115
+ const rect = dialogEl.getBoundingClientRect();
116
+ const inside =
117
+ event.clientX >= rect.left &&
118
+ event.clientX <= rect.right &&
119
+ event.clientY >= rect.top &&
120
+ event.clientY <= rect.bottom;
121
+ if (!inside) close();
122
+ }}
123
+ >
124
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
125
+ <div class="drawer-header" onpointerdown={startDrag}>
126
+ <h2 id="{id}-title" class="drawer-title">{title}</h2>
127
+ {#if description}
128
+ <p id="{id}-description" class="drawer-description">{description}</p>
129
+ {/if}
130
+ </div>
131
+ {#if children}
132
+ <div class="drawer-body">{@render children()}</div>
133
+ {/if}
134
+ {#if actions}
135
+ <div class="drawer-actions">{@render actions(close)}</div>
136
+ {/if}
137
+ <!-- Last in the DOM so the content, not the handle, gets the initial focus -->
138
+ <button
139
+ type="button"
140
+ class="drawer-handle"
141
+ aria-label="Close"
142
+ onpointerdown={startDrag}
143
+ onclick={() => {
144
+ if (!dragged) close();
145
+ }}
146
+ >
147
+ <span class="drawer-handle-bar"></span>
148
+ <X class="drawer-handle-icon" aria-hidden="true" />
149
+ </button>
150
+ </dialog>
151
+
152
+ <style>
153
+ /* Locks page scrolling while open; the gutter keeps the layout from shifting */
154
+ :global(html:has(.drawer[open])) {
155
+ overflow: hidden;
156
+ scrollbar-gutter: stable;
157
+ }
158
+
159
+ .drawer {
160
+ box-sizing: border-box;
161
+ width: 100%;
162
+ max-width: 560px;
163
+ max-height: calc(100dvh - 48px);
164
+ flex-direction: column;
165
+ /* Pinned to the bottom edge */
166
+ margin: auto auto 0;
167
+ padding: 0 0 env(safe-area-inset-bottom);
168
+ overflow: hidden;
169
+ background-color: var(--color-bg);
170
+ color: var(--color-text);
171
+ border: 1px solid var(--color-border);
172
+ border-bottom: 0;
173
+ border-radius: 16px 16px 0 0;
174
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.12);
175
+ outline: none;
176
+ translate: 0 100%;
177
+ transition:
178
+ translate 260ms cubic-bezier(0.32, 0.72, 0, 1),
179
+ display 260ms allow-discrete,
180
+ overlay 260ms allow-discrete;
181
+ }
182
+
183
+ .drawer[open] {
184
+ display: flex;
185
+ translate: 0 0;
186
+ }
187
+
188
+ /* Follows the finger directly */
189
+ .drawer--dragging {
190
+ transition: none;
191
+ }
192
+
193
+ .drawer::backdrop {
194
+ background-color: rgba(0, 0, 0, 0);
195
+ transition:
196
+ background-color 260ms ease,
197
+ display 260ms allow-discrete,
198
+ overlay 260ms allow-discrete;
199
+ }
200
+
201
+ .drawer[open]::backdrop {
202
+ background-color: rgba(0, 0, 0, 0.4);
203
+ }
204
+
205
+ @starting-style {
206
+ .drawer[open] {
207
+ translate: 0 100%;
208
+ }
209
+
210
+ .drawer[open]::backdrop {
211
+ background-color: rgba(0, 0, 0, 0);
212
+ }
213
+ }
214
+
215
+ @media (prefers-reduced-motion: reduce) {
216
+ .drawer,
217
+ .drawer::backdrop {
218
+ transition-duration: 0ms;
219
+ }
220
+ }
221
+
222
+ .drawer-header {
223
+ flex-shrink: 0;
224
+ /* Room for the handle */
225
+ padding: 28px 20px 12px;
226
+ cursor: grab;
227
+ user-select: none;
228
+ /* Dragging must not scroll the page or get cancelled by the browser */
229
+ touch-action: none;
230
+ }
231
+
232
+ .drawer--dragging .drawer-header,
233
+ .drawer--dragging .drawer-handle {
234
+ cursor: grabbing;
235
+ }
236
+
237
+ .drawer-title {
238
+ margin: 0;
239
+ font-size: 15px;
240
+ font-weight: 600;
241
+ }
242
+
243
+ .drawer-description {
244
+ margin: 6px 0 0;
245
+ font-size: 13px;
246
+ line-height: 1.5;
247
+ color: var(--color-text-muted);
248
+ }
249
+
250
+ .drawer-body {
251
+ min-height: 0;
252
+ padding: 4px 20px 20px;
253
+ overflow-y: auto;
254
+ overscroll-behavior: contain;
255
+ font-size: 13px;
256
+ }
257
+
258
+ /* Stacked, the primary action (last in the markup) on top */
259
+ .drawer-actions {
260
+ display: flex;
261
+ flex-shrink: 0;
262
+ flex-direction: column-reverse;
263
+ gap: 8px;
264
+ padding: 4px 20px 20px;
265
+ }
266
+
267
+ .drawer-actions :global(.button) {
268
+ width: 100%;
269
+ }
270
+
271
+ .drawer-handle {
272
+ position: absolute;
273
+ top: 0;
274
+ left: 50%;
275
+ padding: 10px 24px;
276
+ background: none;
277
+ border: 0;
278
+ border-radius: 8px;
279
+ cursor: grab;
280
+ translate: -50% 0;
281
+ touch-action: none;
282
+ -webkit-tap-highlight-color: transparent;
283
+ }
284
+
285
+ .drawer-handle:focus-visible {
286
+ outline: 2px solid var(--color-accent);
287
+ outline-offset: -4px;
288
+ }
289
+
290
+ .drawer-handle-bar {
291
+ display: block;
292
+ width: 36px;
293
+ height: 4px;
294
+ background-color: var(--color-border-strong);
295
+ border-radius: 2px;
296
+ transition: background-color 120ms ease;
297
+ }
298
+
299
+ .drawer-handle:hover .drawer-handle-bar {
300
+ background-color: var(--color-text-faint);
301
+ }
302
+
303
+ .drawer-handle :global(.drawer-handle-icon) {
304
+ display: none;
305
+ width: 16px;
306
+ height: 16px;
307
+ }
308
+
309
+ /* Larger screens: a floating panel on the right, with a close button instead of the handle */
310
+ @media (min-width: 768px) {
311
+ .drawer {
312
+ width: 400px;
313
+ max-width: calc(100% - 48px);
314
+ height: calc(100dvh - 2 * var(--drawer-inset));
315
+ max-height: none;
316
+ /* Floats with a gap to the screen edges */
317
+ --drawer-inset: 8px;
318
+ margin: var(--drawer-inset) var(--drawer-inset) var(--drawer-inset) auto;
319
+ padding: 0;
320
+ border: 1px solid var(--color-border);
321
+ border-radius: 20px;
322
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.16);
323
+ translate: calc(100% + var(--drawer-inset)) 0;
324
+ }
325
+
326
+ @starting-style {
327
+ .drawer[open] {
328
+ translate: calc(100% + var(--drawer-inset)) 0;
329
+ }
330
+ }
331
+
332
+ .drawer-header {
333
+ padding: 20px 56px 12px 20px;
334
+ }
335
+
336
+ /* Pushes the actions to the bottom */
337
+ .drawer-body {
338
+ flex: 1;
339
+ }
340
+
341
+ .drawer-handle {
342
+ top: 14px;
343
+ right: 14px;
344
+ left: auto;
345
+ padding: 6px;
346
+ color: var(--color-text-muted);
347
+ cursor: pointer;
348
+ translate: none;
349
+ }
350
+
351
+ .drawer-handle:hover {
352
+ color: var(--color-text);
353
+ background-color: var(--color-surface);
354
+ }
355
+
356
+ .drawer-handle:focus-visible {
357
+ outline-offset: 2px;
358
+ }
359
+
360
+ .drawer-handle-bar {
361
+ display: none;
362
+ }
363
+
364
+ .drawer-handle :global(.drawer-handle-icon) {
365
+ display: block;
366
+ }
367
+ }
368
+ </style>
@@ -0,0 +1,23 @@
1
+ export type DrawerTriggerProps = {
2
+ onclick: () => void;
3
+ 'aria-haspopup': 'dialog';
4
+ };
5
+ import type { Snippet } from 'svelte';
6
+ type Props = {
7
+ /**
8
+ * Whether the drawer is open. Bindable. A sheet from the bottom on phones, a panel on the right from 768px up.
9
+ * Escape, the handle or close button, dragging it away and a click on the backdrop close it.
10
+ */
11
+ open?: boolean;
12
+ title: string;
13
+ description?: string;
14
+ /** Renders the trigger. Spread the given props onto it, e.g. `<Button {...props} />`. Omit to control `open` yourself. */
15
+ trigger?: Snippet<[DrawerTriggerProps]>;
16
+ /** The drawer's content; scrolls when it is taller than the screen. The first focusable element in it gets the focus on open. */
17
+ children?: Snippet;
18
+ /** Optional Buttons in the footer, stacked at full width with the primary one last; it ends up on top. Call `close` to close the drawer. */
19
+ actions?: Snippet<[close: () => void]>;
20
+ };
21
+ declare const Drawer: import("svelte").Component<Props, {}, "open">;
22
+ type Drawer = ReturnType<typeof Drawer>;
23
+ export default Drawer;