@fuzdev/fuz_ui 0.199.0 → 0.201.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 +1 -0
- package/dist/ApiDeclarationList.svelte.d.ts.map +1 -1
- package/dist/ApiModule.svelte +2 -2
- package/dist/DeclarationDetail.svelte +185 -125
- 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 +140 -0
- package/dist/DialogContent.svelte.d.ts +69 -0
- package/dist/DialogContent.svelte.d.ts.map +1 -0
- package/dist/ProjectLinks.svelte +1 -1
- package/dist/api_search.svelte.d.ts.map +1 -1
- package/dist/api_search.svelte.js +4 -2
- package/dist/declaration.svelte.d.ts +11 -0
- package/dist/declaration.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.js +13 -1
- package/dist/dialog.d.ts +32 -18
- package/dist/dialog.d.ts.map +1 -1
- package/dist/dialog.js +3 -12
- package/dist/module.svelte.d.ts +2 -0
- package/dist/module.svelte.d.ts.map +1 -1
- package/dist/module.svelte.js +2 -0
- package/package.json +6 -6
- package/src/lib/api_search.svelte.ts +4 -2
- package/src/lib/declaration.svelte.ts +16 -1
- package/src/lib/dialog.ts +30 -27
- package/src/lib/module.svelte.ts +4 -0
- package/dist/Dialogs.svelte +0 -28
- package/dist/Dialogs.svelte.d.ts +0 -11
- package/dist/Dialogs.svelte.d.ts.map +0 -1
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>
|
package/dist/Dialog.svelte.d.ts
CHANGED
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type {
|
|
3
|
-
type
|
|
4
|
-
|
|
2
|
+
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
3
|
+
import { type DialogContext, type DialogAlign } from './dialog.js';
|
|
4
|
+
type $$ComponentProps = Omit<SvelteHTMLElements['dialog'], 'children' | 'onclose'> & {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Whether the dialog is shown. When the `<dialog>` mounts it opens via
|
|
7
|
+
* `showModal()`; when it unmounts it closes.
|
|
8
|
+
* Defaults to `true` so the `{#if opened}<Dialog>...</Dialog>{/if}` pattern
|
|
9
|
+
* works without passing `show` -- mounting the component opens the dialog.
|
|
10
|
+
* Pass `show={opened}` to skip the outer `{#if}` and let the component manage
|
|
11
|
+
* its own conditional rendering.
|
|
12
|
+
* @default true
|
|
7
13
|
*/
|
|
8
|
-
|
|
14
|
+
show?: boolean;
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* How the content is aligned in the viewport. `center` vertically centers it;
|
|
17
|
+
* `top` aligns it to the top and grows downward, which avoids jank when the
|
|
18
|
+
* content's height changes.
|
|
19
|
+
* @default 'center'
|
|
12
20
|
*/
|
|
13
|
-
|
|
21
|
+
align?: DialogAlign;
|
|
14
22
|
/**
|
|
23
|
+
* Whether clicking outside the content (see `content_selector`) closes the
|
|
24
|
+
* dialog. `Escape` closes it regardless of this.
|
|
15
25
|
* @default true
|
|
16
26
|
*/
|
|
17
|
-
|
|
27
|
+
dismissable?: boolean;
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
29
|
+
* Fallback selector for a content surface you render in `children` yourself
|
|
30
|
+
* (rather than via `DialogContent`, which self-registers).
|
|
31
|
+
* When `dismissable`, a press that isn't inside a registered surface (e.g.
|
|
32
|
+
* `DialogContent`, which self-registers) and doesn't match this selector
|
|
33
|
+
* closes the dialog. Defaults to the fuz_css `.pane` card; set it to match
|
|
34
|
+
* your surface's outermost element -- with no registered surface and no
|
|
35
|
+
* match, presses anywhere close the dialog.
|
|
21
36
|
* @default '.pane'
|
|
22
37
|
*/
|
|
23
|
-
content_selector?: string
|
|
38
|
+
content_selector?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Called before a user-initiated close (`Escape`, click-outside, or `close`).
|
|
41
|
+
* Return `false` to veto and keep the dialog open -- e.g. to confirm
|
|
42
|
+
* discarding unsaved changes. Programmatic close via `show={false}` bypasses
|
|
43
|
+
* this.
|
|
44
|
+
*/
|
|
45
|
+
onbeforeclose?: () => boolean | void;
|
|
46
|
+
/**
|
|
47
|
+
* Called when the dialog closes -- via `Escape`, click-outside, or `close`.
|
|
48
|
+
* Use it to sync your own open state, e.g. `onclose={() => (opened = false)}`.
|
|
49
|
+
* Like `onbeforeclose`, programmatic close via `show={false}` bypasses this.
|
|
50
|
+
*/
|
|
24
51
|
onclose?: () => void;
|
|
25
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Rendered inside the dialog overlay. Receives the `DialogContext` (e.g.
|
|
54
|
+
* `{close}`); pair with `DialogContent` or render your own surface.
|
|
55
|
+
*/
|
|
56
|
+
children: Snippet<[dialog: DialogContext]>;
|
|
26
57
|
};
|
|
27
58
|
declare const Dialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
28
59
|
type Dialog = ReturnType<typeof Dialog>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Dialog.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Dialog.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;AAIxD,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,WAAW,EAAC,MAAM,aAAa,CAAC;AAEhF,KAAK,gBAAgB,GAAI,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG;IACrF;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;CAC3C,CAAC;AAsHH,QAAA,MAAM,MAAM,sDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {Snippet} from 'svelte';
|
|
3
|
+
import type {HTMLAttributes, SvelteHTMLElements} from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
import {dialog_context, type DialogContext, type DialogCloseButtonAttrs} from './dialog.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The default content surface for `Dialog`: a fuz_css `.pane` card with a
|
|
9
|
+
* `gutter` margin (the dismiss zone), centered in the viewport by `Dialog`'s
|
|
10
|
+
* own layout. It reads `close` from `dialog_context` (set by `Dialog`) and
|
|
11
|
+
* passes it to `children`, so content can close the dialog without the consumer
|
|
12
|
+
* threading `close` down from `Dialog`'s own `children` snippet.
|
|
13
|
+
*
|
|
14
|
+
* Must be rendered inside a `Dialog`, with which it registers its surface, so
|
|
15
|
+
* click-outside-to-close treats presses inside the card as inside regardless of
|
|
16
|
+
* classes -- `pane={false}` works without further setup.
|
|
17
|
+
*
|
|
18
|
+
* By default it renders a `close_button` floating just outside the surface's
|
|
19
|
+
* top-right corner. The surface is a containing block (`position: relative`) for
|
|
20
|
+
* that button and any absolutely-positioned content in `children`.
|
|
21
|
+
*
|
|
22
|
+
* The surface has no layout of its own -- it's a plain block, so children flow
|
|
23
|
+
* top-to-bottom in normal document order. Pass `class` through (e.g. `box` for a
|
|
24
|
+
* centered column, `column` for an uncentered one) to add one.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
padding = 'var(--space_xl)',
|
|
31
|
+
pane = true,
|
|
32
|
+
gutter = 'var(--space_xl3)',
|
|
33
|
+
max_width = 'var(--distance_md)',
|
|
34
|
+
close_button = true,
|
|
35
|
+
children,
|
|
36
|
+
...rest
|
|
37
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
38
|
+
/**
|
|
39
|
+
* The content surface's padding, set as an inline style. Defaults to
|
|
40
|
+
* `var(--space_xl)` for a comfortable gutter between the card edge and its
|
|
41
|
+
* content. Set to `''` or `'0'` for no padding (e.g. a flush image or a
|
|
42
|
+
* surface that manages its own spacing).
|
|
43
|
+
* @default 'var(--space_xl)'
|
|
44
|
+
*/
|
|
45
|
+
padding?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether to apply the fuz_css `.pane` card class to the content surface --
|
|
48
|
+
* its opaque background, shadow, and rounded corners. `true` (the default) is
|
|
49
|
+
* the standard dialog card; `false` gives a chromeless surface. Either way the
|
|
50
|
+
* surface registers with `Dialog`, so click-outside-to-close keeps working.
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
pane?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The gutter around the `.pane` card -- its outer margin, and so the area
|
|
56
|
+
* outside the card where a press dismisses the dialog. Set to `''` or `'0'` to remove.
|
|
57
|
+
* @default 'var(--space_xl3)'
|
|
58
|
+
*/
|
|
59
|
+
gutter?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The card's max width. The card shrinks to its content and is capped here,
|
|
62
|
+
* so narrow content stays narrow while wide content doesn't sprawl. Set to
|
|
63
|
+
* `''` for no cap (pure shrink-to-content).
|
|
64
|
+
* @default 'var(--distance_md)'
|
|
65
|
+
*/
|
|
66
|
+
max_width?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The close button floating just outside the content surface's top-right
|
|
69
|
+
* corner. `true` (the default) renders an absolutely-positioned `.sm.plain.icon_button`
|
|
70
|
+
* that closes the dialog; `false` removes it. It renders after
|
|
71
|
+
* `children`, so a content control (or an `autofocus` element) takes initial
|
|
72
|
+
* focus on open rather than the close button.
|
|
73
|
+
*
|
|
74
|
+
* Pass a `Snippet` to render your own. It receives `attrs`
|
|
75
|
+
* (`DialogCloseButtonAttrs`) carrying the default's placement, styling, a11y,
|
|
76
|
+
* and `onclick` (closes the dialog), plus the `DialogContext` (e.g. `{close}`).
|
|
77
|
+
* Spread `attrs` onto a `<button>` to inherit the corner anchoring and override
|
|
78
|
+
* only what differs (e.g. the glyph), or drop it to place the button freely. The
|
|
79
|
+
* surface is a containing block (`position: relative`), so an absolutely-positioned
|
|
80
|
+
* custom button anchors to it.
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
close_button?: boolean | Snippet<[attrs: DialogCloseButtonAttrs, dialog: DialogContext]>;
|
|
84
|
+
/**
|
|
85
|
+
* Rendered inside the content surface. Receives the `DialogContext` (e.g.
|
|
86
|
+
* `{close}`) so content can close the dialog without reaching into `Dialog`'s
|
|
87
|
+
* `children` snippet.
|
|
88
|
+
*
|
|
89
|
+
* The default `close_button` renders as the surface's last child (so a content
|
|
90
|
+
* control or `autofocus` element wins initial focus over it). Because it's a
|
|
91
|
+
* real child, it throws off universal child-spacing selectors: your content's
|
|
92
|
+
* last element is no longer `:last-child` (so `> :not(:last-child)` matches it
|
|
93
|
+
* and `> :last-child` skips it), and `> * + *` matches the button itself. Either
|
|
94
|
+
* scope spacing to your own elements or a class, or wrap your content in a single
|
|
95
|
+
* element so its child selectors no longer see the button.
|
|
96
|
+
*/
|
|
97
|
+
children: Snippet<[dialog: DialogContext]>;
|
|
98
|
+
} = $props();
|
|
99
|
+
|
|
100
|
+
const dialog = dialog_context.get('DialogContent must be rendered inside a Dialog');
|
|
101
|
+
|
|
102
|
+
// the default close button's attributes; `onclick` closes the dialog and the rest
|
|
103
|
+
// is handed to a custom `close_button` snippet so it can inherit the corner-anchored
|
|
104
|
+
// button by spreading them. positioning is inline (not a scoped class) so it travels
|
|
105
|
+
// into the consumer's snippet, which carries its own style scope
|
|
106
|
+
const close_button_attrs: DialogCloseButtonAttrs = {
|
|
107
|
+
type: 'button',
|
|
108
|
+
class: 'sm plain icon_button',
|
|
109
|
+
style: 'position: absolute; top: 0; right: 0;',
|
|
110
|
+
onclick: dialog.close,
|
|
111
|
+
title: 'close',
|
|
112
|
+
'aria-label': 'close',
|
|
113
|
+
} satisfies SvelteHTMLElements['button'];
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<!-- The surface is centered by `Dialog`'s `.dialog-wrapper` flex (no wrapper of its own
|
|
117
|
+
needed) and is a containing block (`position: relative`) for the close button and any
|
|
118
|
+
absolutely-positioned content in `children`. `gutter` is the surface's margin -- the
|
|
119
|
+
dismiss zone outside the card, where a press lands on the wrapper and closes the dialog.
|
|
120
|
+
`min-width: 0` lets this flex-item surface shrink below its content's intrinsic width, so
|
|
121
|
+
content with its own overflow (e.g. `Code`) scrolls instead of forcing the card wider. -->
|
|
122
|
+
<div
|
|
123
|
+
{...rest}
|
|
124
|
+
class:pane
|
|
125
|
+
style:padding
|
|
126
|
+
style:margin={gutter}
|
|
127
|
+
style:max-width={max_width}
|
|
128
|
+
style:min-width="0"
|
|
129
|
+
style:position="relative"
|
|
130
|
+
{@attach dialog.register_surface}
|
|
131
|
+
>
|
|
132
|
+
{@render children(dialog)}
|
|
133
|
+
<!-- rendered after `children` so a content control (or an `autofocus` element)
|
|
134
|
+
takes initial focus on open, not the close button -->
|
|
135
|
+
{#if close_button === true}
|
|
136
|
+
<button {...close_button_attrs}>✕</button>
|
|
137
|
+
{:else if close_button}
|
|
138
|
+
{@render close_button(close_button_attrs, dialog)}
|
|
139
|
+
{/if}
|
|
140
|
+
</div>
|