@fuzdev/fuz_ui 0.200.0 → 0.202.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/README.md +2 -2
- package/dist/ApiDeclarationList.svelte +2 -2
- package/dist/CopyToClipboard.svelte +5 -2
- package/dist/CopyToClipboard.svelte.d.ts.map +1 -1
- package/dist/DeclarationDetail.svelte +40 -35
- package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
- package/dist/Dialog.svelte +174 -165
- package/dist/Dialog.svelte.d.ts +44 -13
- package/dist/Dialog.svelte.d.ts.map +1 -1
- package/dist/DialogContent.svelte +142 -0
- package/dist/DialogContent.svelte.d.ts +69 -0
- package/dist/DialogContent.svelte.d.ts.map +1 -0
- package/dist/Docs.svelte +11 -8
- package/dist/Docs.svelte.d.ts.map +1 -1
- package/dist/DocsSearch.svelte +4 -1
- package/dist/DocsSearch.svelte.d.ts.map +1 -1
- package/dist/Svg.svelte +46 -9
- package/dist/Svg.svelte.d.ts.map +1 -1
- package/dist/dialog.d.ts +32 -18
- package/dist/dialog.d.ts.map +1 -1
- package/dist/dialog.js +3 -12
- package/dist/icons.d.ts +457 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +564 -0
- package/dist/logos.d.ts +1 -3
- package/dist/logos.d.ts.map +1 -1
- package/dist/logos.js +1 -1
- package/dist/svg.d.ts +69 -10
- package/dist/svg.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/lib/dialog.ts +30 -27
- package/src/lib/icons.ts +655 -0
- package/src/lib/logos.ts +1 -1
- package/src/lib/svg.ts +70 -9
- package/dist/Dialogs.svelte +0 -28
- package/dist/Dialogs.svelte.d.ts +0 -11
- package/dist/Dialogs.svelte.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# fuz_ui
|
|
2
2
|
|
|
3
3
|
[<img src="static/logo.svg" alt="a friendly brown spider facing you" align="right" width="192" height="192">](https://ui.fuz.dev/)
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
fuz_ui is a Svelte UI library with components and helpers for making zippy
|
|
8
8
|
websites. It's built on [fuz_css](https://css.fuz.dev/)
|
|
9
|
-
and provides
|
|
9
|
+
and provides a documentation system built on
|
|
10
10
|
[svelte-docinfo](https://svelte-docinfo.fuz.dev/).
|
|
11
11
|
It's in early alpha with breaking changes ahead.
|
|
12
12
|
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
{:else}
|
|
25
25
|
{#each declarations as declaration (`${declaration.module_path}:${declaration.name}`)}
|
|
26
26
|
<TomeSection>
|
|
27
|
-
<!--
|
|
27
|
+
<!-- Show the bare name; generic parameters are documented in the detail below (generics/type signature). -->
|
|
28
28
|
<TomeSectionHeader text={declaration.name}>
|
|
29
|
-
<div class="word-break:break-all">{declaration.
|
|
29
|
+
<div class="word-break:break-all">{declaration.name}</div>
|
|
30
30
|
</TomeSectionHeader>
|
|
31
31
|
<article id={declaration.name}>
|
|
32
32
|
<DeclarationDetail {declaration} />
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import type {SvelteHTMLElements} from 'svelte/elements';
|
|
5
5
|
import {scale, slide} from 'svelte/transition';
|
|
6
6
|
|
|
7
|
+
import {icon_checkmark, icon_copy} from './icons.js';
|
|
8
|
+
import Svg from './Svg.svelte';
|
|
9
|
+
|
|
7
10
|
// TODO @many should this have the Button suffix?
|
|
8
11
|
|
|
9
12
|
// TODO add docs entry, see also PasteFromClipboard.svelte
|
|
@@ -73,8 +76,8 @@
|
|
|
73
76
|
{#if children}
|
|
74
77
|
{@render children(copied, failed)}
|
|
75
78
|
{:else if copied}
|
|
76
|
-
<div in:scale={{duration: 200}}
|
|
79
|
+
<div in:scale={{duration: 200}}><Svg data={icon_checkmark} /></div>
|
|
77
80
|
{:else}
|
|
78
|
-
<div in:slide
|
|
81
|
+
<div in:slide><Svg data={icon_copy} /></div>
|
|
79
82
|
{/if}
|
|
80
83
|
</button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopyToClipboard.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/CopyToClipboard.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"CopyToClipboard.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/CopyToClipboard.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;AAMvD,KAAK,gBAAgB,GAAI,UAAU,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GAAG;IAC/E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD,CAAC;AAkEH,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -62,6 +62,16 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
62
62
|
</section>
|
|
63
63
|
{/snippet}
|
|
64
64
|
|
|
65
|
+
<!-- A compact parameter row: name (with optional marker) and type, for snippet params and overloads. -->
|
|
66
|
+
{#snippet param_row(param: ParameterJsonInput)}
|
|
67
|
+
<div class="row gap_md">
|
|
68
|
+
<code
|
|
69
|
+
>{param.name}{#if param.optional}?{/if}</code
|
|
70
|
+
>
|
|
71
|
+
<TypeLink type={param.type} />
|
|
72
|
+
</div>
|
|
73
|
+
{/snippet}
|
|
74
|
+
|
|
65
75
|
<!-- Compact prose metadata for props and members (deprecated, since, examples, see also, throws). -->
|
|
66
76
|
{#snippet doc_extras(item: DocExtras)}
|
|
67
77
|
<!-- eslint-disable-next-line @typescript-eslint/no-deprecated -->
|
|
@@ -104,6 +114,12 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
104
114
|
{/if}
|
|
105
115
|
{/snippet}
|
|
106
116
|
|
|
117
|
+
<!-- A comma-separated inline list of type links (extends, implements, intersects are rarely more than one). -->
|
|
118
|
+
{#snippet type_list(types: Array<string>)}
|
|
119
|
+
{#each types as type, i (type)}{#if i > 0},
|
|
120
|
+
{/if}<TypeLink {type} />{/each}
|
|
121
|
+
{/snippet}
|
|
122
|
+
|
|
107
123
|
<!-- Metadata -->
|
|
108
124
|
<p class="row justify-content:space-between">
|
|
109
125
|
<ModuleLink module_path={declaration.module_path} />
|
|
@@ -211,12 +227,7 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
211
227
|
<section>
|
|
212
228
|
<h5>snippet parameters</h5>
|
|
213
229
|
{#each prop.parameters as param (param)}
|
|
214
|
-
|
|
215
|
-
<code
|
|
216
|
-
>{param.name}{#if param.optional}?{/if}</code
|
|
217
|
-
>
|
|
218
|
-
<TypeLink type={param.type} />
|
|
219
|
-
</div>
|
|
230
|
+
{@render param_row(param)}
|
|
220
231
|
{/each}
|
|
221
232
|
</section>
|
|
222
233
|
{/if}
|
|
@@ -238,12 +249,7 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
238
249
|
{/if}
|
|
239
250
|
{#if overload.parameters?.length}
|
|
240
251
|
{#each overload.parameters as param (param)}
|
|
241
|
-
|
|
242
|
-
<code
|
|
243
|
-
>{param.name}{#if param.optional}?{/if}</code
|
|
244
|
-
>
|
|
245
|
-
<TypeLink type={param.type} />
|
|
246
|
-
</div>
|
|
252
|
+
{@render param_row(param)}
|
|
247
253
|
{/each}
|
|
248
254
|
{/if}
|
|
249
255
|
{#if overload.returnType}
|
|
@@ -264,11 +270,9 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
264
270
|
{#if declaration.intersects?.length}
|
|
265
271
|
<section>
|
|
266
272
|
<h4>intersects</h4>
|
|
267
|
-
<
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
{/each}
|
|
271
|
-
</ul>
|
|
273
|
+
<div class="row gap_md flex-wrap:wrap">
|
|
274
|
+
{@render type_list(declaration.intersects)}
|
|
275
|
+
</div>
|
|
272
276
|
</section>
|
|
273
277
|
{/if}
|
|
274
278
|
|
|
@@ -286,10 +290,13 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
286
290
|
<!-- generics -->
|
|
287
291
|
{#if declaration.generic_params.length}
|
|
288
292
|
<section>
|
|
289
|
-
<
|
|
293
|
+
<div class="row gap_md">
|
|
294
|
+
<h4>generics</h4>
|
|
295
|
+
<TypeLink type={declaration.display_name} />
|
|
296
|
+
</div>
|
|
290
297
|
{#each declaration.generic_params as generic (generic)}
|
|
291
298
|
<section>
|
|
292
|
-
<
|
|
299
|
+
<h5><code>{generic.name}</code></h5>
|
|
293
300
|
{#if generic.constraint}
|
|
294
301
|
<div class="row gap_md">
|
|
295
302
|
<strong>constraint</strong>
|
|
@@ -312,27 +319,19 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
312
319
|
<section>
|
|
313
320
|
<h4>inheritance</h4>
|
|
314
321
|
{#if declaration.extends_type}
|
|
315
|
-
<div>
|
|
322
|
+
<div class="row gap_md flex-wrap:wrap">
|
|
316
323
|
<strong>extends:</strong>
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
</ul>
|
|
323
|
-
{:else}
|
|
324
|
-
<TypeLink type={declaration.extends_type} />
|
|
325
|
-
{/if}
|
|
324
|
+
{@render type_list(
|
|
325
|
+
Array.isArray(declaration.extends_type)
|
|
326
|
+
? declaration.extends_type
|
|
327
|
+
: [declaration.extends_type],
|
|
328
|
+
)}
|
|
326
329
|
</div>
|
|
327
330
|
{/if}
|
|
328
331
|
{#if declaration.implements_types?.length}
|
|
329
|
-
<div>
|
|
332
|
+
<div class="row gap_md flex-wrap:wrap">
|
|
330
333
|
<strong>implements:</strong>
|
|
331
|
-
|
|
332
|
-
{#each declaration.implements_types as impl (impl)}
|
|
333
|
-
<li><TypeLink type={impl} /></li>
|
|
334
|
-
{/each}
|
|
335
|
-
</ul>
|
|
334
|
+
{@render type_list(declaration.implements_types)}
|
|
336
335
|
</div>
|
|
337
336
|
{/if}
|
|
338
337
|
</section>
|
|
@@ -465,3 +464,9 @@ including parameters, props, members, overloads, intersects, and more.
|
|
|
465
464
|
{/each}
|
|
466
465
|
</section>
|
|
467
466
|
{/if}
|
|
467
|
+
|
|
468
|
+
<style>
|
|
469
|
+
section section:not(:last-child) {
|
|
470
|
+
margin-bottom: var(--space_xl4);
|
|
471
|
+
}
|
|
472
|
+
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeclarationDetail.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/DeclarationDetail.svelte"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AAiBxD,KAAK,gBAAgB,GAAI;IAAC,WAAW,EAAE,WAAW,CAAA;CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"DeclarationDetail.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/DeclarationDetail.svelte"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AAiBxD,KAAK,gBAAgB,GAAI;IAAC,WAAW,EAAE,WAAW,CAAA;CAAC,CAAC;AAqbrD;;;;;;;;GAQG;AACH,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
package/dist/Dialog.svelte
CHANGED
|
@@ -1,167 +1,177 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type {Snippet} from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
import type {SvelteHTMLElements} from 'svelte/elements';
|
|
4
|
+
import {SvelteSet} from 'svelte/reactivity';
|
|
5
|
+
import {swallow} from '@fuzdev/fuz_util/dom.js';
|
|
6
|
+
|
|
7
|
+
import {dialog_context, type DialogContext, type DialogAlign} from './dialog.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This component renders a native `<dialog>` opened with `showModal()`, which
|
|
11
|
+
* puts it in the top layer. The top layer escapes ancestor stacking/overflow
|
|
12
|
+
* contexts (so no `Teleport` is needed), traps focus, makes the rest of the page
|
|
13
|
+
* inert, closes on Escape, and restores focus to the previously focused element
|
|
14
|
+
* on close -- all natively. The dim background is the native `::backdrop`.
|
|
15
|
+
*
|
|
16
|
+
* We render a full-viewport overlay inside the dialog rather than a content-sized
|
|
17
|
+
* box, to preserve the scrolling and `align="top"` behaviors. The content
|
|
18
|
+
* surface itself is the consumer's -- pair this with `DialogContent` for the
|
|
19
|
+
* default `.pane` card and gutter, or render your own surface in `children`.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
23
|
|
|
24
24
|
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
active = true,
|
|
25
|
+
show = true,
|
|
26
|
+
align = 'center',
|
|
27
|
+
dismissable = true,
|
|
29
28
|
content_selector = '.pane',
|
|
29
|
+
onbeforeclose,
|
|
30
30
|
onclose,
|
|
31
31
|
children,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
container?: HTMLElement;
|
|
32
|
+
...rest
|
|
33
|
+
}: Omit<SvelteHTMLElements['dialog'], 'children' | 'onclose'> & {
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
35
|
+
* Whether the dialog is shown. When the `<dialog>` mounts it opens via
|
|
36
|
+
* `showModal()`; when it unmounts it closes.
|
|
37
|
+
* Defaults to `true` so the `{#if opened}<Dialog>...</Dialog>{/if}` pattern
|
|
38
|
+
* works without passing `show` -- mounting the component opens the dialog.
|
|
39
|
+
* Pass `show={opened}` to skip the outer `{#if}` and let the component manage
|
|
40
|
+
* its own conditional rendering.
|
|
41
|
+
* @default true
|
|
37
42
|
*/
|
|
38
|
-
|
|
43
|
+
show?: boolean;
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
45
|
+
* How the content is aligned in the viewport. `center` vertically centers it;
|
|
46
|
+
* `top` aligns it to the top and grows downward, which avoids jank when the
|
|
47
|
+
* content's height changes.
|
|
48
|
+
* @default 'center'
|
|
42
49
|
*/
|
|
43
|
-
|
|
50
|
+
align?: DialogAlign;
|
|
44
51
|
/**
|
|
52
|
+
* Whether clicking outside the content (see `content_selector`) closes the
|
|
53
|
+
* dialog. `Escape` closes it regardless of this.
|
|
45
54
|
* @default true
|
|
46
55
|
*/
|
|
47
|
-
|
|
56
|
+
dismissable?: boolean;
|
|
48
57
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
58
|
+
* Fallback selector for a content surface you render in `children` yourself
|
|
59
|
+
* (rather than via `DialogContent`, which self-registers).
|
|
60
|
+
* When `dismissable`, a press that isn't inside a registered surface (e.g.
|
|
61
|
+
* `DialogContent`, which self-registers) and doesn't match this selector
|
|
62
|
+
* closes the dialog. Defaults to the fuz_css `.pane` card; set it to match
|
|
63
|
+
* your surface's outermost element -- with no registered surface and no
|
|
64
|
+
* match, presses anywhere close the dialog.
|
|
51
65
|
* @default '.pane'
|
|
52
66
|
*/
|
|
53
|
-
content_selector?: string
|
|
67
|
+
content_selector?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Called before a user-initiated close (`Escape`, click-outside, or `close`).
|
|
70
|
+
* Return `false` to veto and keep the dialog open -- e.g. to confirm
|
|
71
|
+
* discarding unsaved changes. Programmatic close via `show={false}` bypasses
|
|
72
|
+
* this.
|
|
73
|
+
*/
|
|
74
|
+
onbeforeclose?: () => boolean | void;
|
|
75
|
+
/**
|
|
76
|
+
* Called when the dialog closes -- via `Escape`, click-outside, or `close`.
|
|
77
|
+
* Use it to sync your own open state, e.g. `onclose={() => (opened = false)}`.
|
|
78
|
+
* Like `onbeforeclose`, programmatic close via `show={false}` bypasses this.
|
|
79
|
+
*/
|
|
54
80
|
onclose?: () => void;
|
|
55
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Rendered inside the dialog overlay. Receives the `DialogContext` (e.g.
|
|
83
|
+
* `{close}`); pair with `DialogContent` or render your own surface.
|
|
84
|
+
*/
|
|
85
|
+
children: Snippet<[dialog: DialogContext]>;
|
|
56
86
|
} = $props();
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const root_el = document.querySelector(ROOT_SELECTOR);
|
|
75
|
-
if (!root_el) {
|
|
76
|
-
throw Error(`Cannot find dialog root element with selector '${ROOT_SELECTOR}'`);
|
|
77
|
-
}
|
|
78
|
-
container_el = document.createElement('div');
|
|
79
|
-
container_el.id = CONTAINER_ID;
|
|
80
|
-
container_el.style.display = 'contents';
|
|
81
|
-
root_el.appendChild(container_el);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
88
|
+
let dialog_el: HTMLDialogElement | undefined;
|
|
89
|
+
|
|
90
|
+
// Guards against a single dismissal firing `onclose` twice. Reset on mount.
|
|
91
|
+
let closing = false;
|
|
92
|
+
|
|
93
|
+
// Closes the native dialog (which restores focus to the trigger) and notifies
|
|
94
|
+
// the consumer to unmount us. Closing here, while still mounted and connected,
|
|
95
|
+
// is what makes focus restoration work -- closing on unmount would run on a
|
|
96
|
+
// detached node, which doesn't restore focus.
|
|
97
|
+
const request_close = () => {
|
|
98
|
+
if (closing) return;
|
|
99
|
+
// let the consumer veto a user-initiated dismissal (e.g. confirm unsaved changes)
|
|
100
|
+
if (onbeforeclose?.() === false) return;
|
|
101
|
+
closing = true;
|
|
102
|
+
dialog_el?.close();
|
|
103
|
+
onclose?.();
|
|
84
104
|
};
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
let content_el: HTMLElement | undefined = $state.raw();
|
|
88
|
-
|
|
106
|
+
// The `close` passed to children also swallows the triggering event.
|
|
89
107
|
const close = (e?: Event) => {
|
|
90
108
|
if (e) swallow(e);
|
|
91
|
-
|
|
109
|
+
request_close();
|
|
92
110
|
};
|
|
93
111
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
}
|
|
112
|
+
// Content surfaces (e.g. `DialogContent`) register here so a press inside one
|
|
113
|
+
// isn't an outside-dismiss. Identity-based, so it works regardless of classes;
|
|
114
|
+
// `content_selector` stays the fallback for surfaces rendered in `children`.
|
|
115
|
+
const surfaces = new SvelteSet<Element>();
|
|
116
|
+
const register_surface = (element: Element): (() => void) => {
|
|
117
|
+
surfaces.add(element);
|
|
118
|
+
return () => {
|
|
119
|
+
surfaces.delete(element);
|
|
120
|
+
};
|
|
105
121
|
};
|
|
106
122
|
|
|
107
|
-
// The
|
|
108
|
-
//
|
|
109
|
-
|
|
123
|
+
// The context is both passed to `children` and set for descendants (e.g.
|
|
124
|
+
// `DialogContent`), so either path can close the dialog.
|
|
125
|
+
const context: DialogContext = {close, register_surface};
|
|
126
|
+
dialog_context.set(context);
|
|
127
|
+
|
|
128
|
+
const setup_dialog = (el: HTMLDialogElement) => {
|
|
129
|
+
dialog_el = el;
|
|
130
|
+
closing = false;
|
|
131
|
+
// Esc (and any platform close request) fires `cancel`. We swallow it and
|
|
132
|
+
// close the dialog ourselves (above, while connected) so focus is restored,
|
|
133
|
+
// and so the consumer's state stays in sync and we unmount.
|
|
134
|
+
const oncancel = (e: Event) => {
|
|
135
|
+
swallow(e);
|
|
136
|
+
request_close();
|
|
137
|
+
};
|
|
138
|
+
el.addEventListener('cancel', oncancel);
|
|
139
|
+
if (!el.open) el.showModal();
|
|
140
|
+
return () => {
|
|
141
|
+
el.removeEventListener('cancel', oncancel);
|
|
142
|
+
dialog_el = undefined;
|
|
143
|
+
// fallback for programmatic unmount (e.g. `show` set false directly)
|
|
144
|
+
if (el.open) el.close();
|
|
145
|
+
};
|
|
146
|
+
};
|
|
110
147
|
</script>
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
For more see: https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
|
|
119
|
-
and https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
|
|
120
|
-
-->
|
|
121
|
-
<Teleport
|
|
122
|
-
to={container_el}
|
|
123
|
-
onmove={async () => {
|
|
124
|
-
await wait(); // TODO this is a hack to get animations working, `Teleport` now mounts synchronously?!
|
|
125
|
-
ready = true;
|
|
126
|
-
dialog_el?.focus(); // TODO make this more declarative? probably want to focus only after moving though, not on mount, which makes an action trickier
|
|
127
|
-
}}
|
|
128
|
-
>
|
|
129
|
-
<div
|
|
130
|
-
class="dialog"
|
|
131
|
-
class:ready
|
|
132
|
-
class:layout-page={layout === 'page'}
|
|
133
|
-
role="dialog"
|
|
134
|
-
aria-modal="true"
|
|
135
|
-
bind:this={dialog_el}
|
|
136
|
-
tabindex="-1"
|
|
137
|
-
style:z-index={100 + index}
|
|
149
|
+
{#if show}
|
|
150
|
+
<dialog
|
|
151
|
+
{...rest}
|
|
152
|
+
class="dialog {rest.class}"
|
|
153
|
+
class:align-top={align === 'top'}
|
|
154
|
+
{@attach setup_dialog}
|
|
138
155
|
>
|
|
139
|
-
<div
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
>
|
|
156
|
-
<div class="dialog-bg" aria-hidden="true"></div>
|
|
157
|
-
<div class="dialog-content" bind:this={content_el}>
|
|
158
|
-
<!-- mount the content only after teleporting to avoid issues -->
|
|
159
|
-
{#if ready}{@render children(close)}{/if}
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
156
|
+
<div
|
|
157
|
+
class="dialog-wrapper"
|
|
158
|
+
role="none"
|
|
159
|
+
onmousedown={(e) => {
|
|
160
|
+
if (!dismissable) return;
|
|
161
|
+
const target = e.target as Element;
|
|
162
|
+
// inside a registered surface (e.g. DialogContent) -> not an outside dismiss
|
|
163
|
+
for (const surface of surfaces) {
|
|
164
|
+
if (surface.contains(target)) return;
|
|
165
|
+
}
|
|
166
|
+
// fallback for surfaces rendered in `children`: match `content_selector`
|
|
167
|
+
if (content_selector && target.closest(content_selector)) return;
|
|
168
|
+
close(e);
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{@render children(context)}
|
|
162
172
|
</div>
|
|
163
|
-
</
|
|
164
|
-
|
|
173
|
+
</dialog>
|
|
174
|
+
{/if}
|
|
165
175
|
|
|
166
176
|
<style>
|
|
167
177
|
.dialog {
|
|
@@ -171,51 +181,50 @@
|
|
|
171
181
|
var(--shadow_color, var(--shadow_color_umbra)) var(--shadow_alpha_70),
|
|
172
182
|
transparent
|
|
173
183
|
);
|
|
184
|
+
/* reset the user-agent dialog styles; we render a full-viewport overlay */
|
|
185
|
+
max-width: none;
|
|
186
|
+
max-height: none;
|
|
187
|
+
/* width/height are needed despite `inset: 0` to make the dialog element fill the viewport */
|
|
188
|
+
width: 100%;
|
|
189
|
+
height: 100%;
|
|
190
|
+
margin: 0;
|
|
191
|
+
padding: 0;
|
|
192
|
+
border: none;
|
|
193
|
+
background: transparent;
|
|
194
|
+
color: inherit;
|
|
174
195
|
position: fixed;
|
|
175
196
|
inset: 0;
|
|
176
197
|
overflow: auto;
|
|
177
|
-
/* this simplifies the code a lot but doesn't prevent scrolling
|
|
178
|
-
the underlying content when the dialog doesn't overflow, even when `overflow: scroll`
|
|
179
|
-
TODO check if this behaves as desired after switching to use the `dialog` element */
|
|
180
198
|
overscroll-behavior: contain;
|
|
181
199
|
}
|
|
200
|
+
/* the native backdrop is the dim background; fade it in on open */
|
|
201
|
+
.dialog::backdrop {
|
|
202
|
+
background-color: var(--dialog_bg, var(--darken_60));
|
|
203
|
+
transition: background-color var(--duration_2) ease;
|
|
204
|
+
}
|
|
205
|
+
@starting-style {
|
|
206
|
+
.dialog::backdrop {
|
|
207
|
+
background-color: transparent;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
182
210
|
|
|
211
|
+
/* Lock background scroll while a modal dialog is open. `scrollbar-gutter`
|
|
212
|
+
reserves the scrollbar space so toggling the lock doesn't shift the layout. */
|
|
213
|
+
:global(html:has(dialog.dialog[open])) {
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
scrollbar-gutter: stable;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* `min-height: 100%` (not `height`) makes tall content overflow downward only: the
|
|
219
|
+
wrapper grows to its content, so the centered top stays reachable in the dialog's
|
|
220
|
+
scroll. A fixed-height centering box would strand the top above the scroll origin. */
|
|
183
221
|
.dialog-wrapper {
|
|
184
|
-
position: relative; /* for the surface */
|
|
185
222
|
min-height: 100%;
|
|
186
223
|
display: flex;
|
|
187
224
|
align-items: center;
|
|
188
225
|
justify-content: center;
|
|
189
226
|
}
|
|
190
|
-
.
|
|
227
|
+
.align-top .dialog-wrapper {
|
|
191
228
|
align-items: start;
|
|
192
229
|
}
|
|
193
|
-
|
|
194
|
-
.dialog-bg {
|
|
195
|
-
position: absolute;
|
|
196
|
-
inset: 0;
|
|
197
|
-
z-index: 0;
|
|
198
|
-
opacity: 0;
|
|
199
|
-
transition: opacity var(--duration_3) ease;
|
|
200
|
-
background-color: var(--dialog_bg, var(--darken_60));
|
|
201
|
-
}
|
|
202
|
-
.ready .dialog-bg {
|
|
203
|
-
opacity: 1;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.dialog-layout {
|
|
207
|
-
height: 100%;
|
|
208
|
-
/* makes the content overflow downwards instead of upwards+downwards because it's centered */
|
|
209
|
-
max-height: 100%;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.dialog-content {
|
|
213
|
-
width: 100%;
|
|
214
|
-
transform: scale(0.99);
|
|
215
|
-
transition: transform var(--duration_1) ease;
|
|
216
|
-
padding: 40px;
|
|
217
|
-
}
|
|
218
|
-
.ready .dialog-content {
|
|
219
|
-
transform: scale(1);
|
|
220
|
-
}
|
|
221
230
|
</style>
|