@fuzdev/fuz_ui 0.200.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 CHANGED
@@ -1,4 +1,4 @@
1
- # Fuz
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 includes a documentation system built on
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
- <!-- `text` drives the slug/anchor (bare name); display shows generics via `display_name` -->
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.display_name}</div>
29
+ <div class="word-break:break-all">{declaration.name}</div>
30
30
  </TomeSectionHeader>
31
31
  <article id={declaration.name}>
32
32
  <DeclarationDetail {declaration} />
@@ -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
- <div class="row gap_md">
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
- <div class="row gap_md">
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
- <ul>
268
- {#each declaration.intersects as type (type)}
269
- <li><TypeLink {type} /></li>
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
- <h4>generics</h4>
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
- <h4><code>{generic.name}</code></h4>
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
- {#if Array.isArray(declaration.extends_type)}
318
- <ul>
319
- {#each declaration.extends_type as ext (ext)}
320
- <li><TypeLink type={ext} /></li>
321
- {/each}
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
- <ul>
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;AAmbrD;;;;;;;;GAQG;AACH,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,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"}
@@ -1,167 +1,177 @@
1
1
  <script lang="ts">
2
2
  import type {Snippet} from 'svelte';
3
- import {is_editable, swallow} from '@fuzdev/fuz_util/dom.js';
4
- import {wait} from '@fuzdev/fuz_util/async.js';
5
-
6
- import Teleport from './Teleport.svelte';
7
- import type {DialogLayout} from './dialog.js';
8
-
9
- // TODO use `<dialog>` here instead of `Teleport`
10
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
11
-
12
- /*
13
-
14
- This component is a singleton that mounts to a #dialog container element added to the body,
15
- using a `Teleport` component to avoid ancestor-caused style issues like overflow and z-index.
16
- It adds `.dialog` to the body when mounted to fix scrolling behavior,
17
- and adds padding to the body to adjust for any scrollbars.
18
- It uses a CSS custom property for this to avoid the high complexity of trying to
19
- correctly revert any preexisting values for overflow and padding on the body.
20
- We don't want to add restrictions to what users can do to the body on their own!
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
- container,
26
- layout = 'centered',
27
- index = 0,
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
- // TODO maybe change this API away from an element to a selector? or remove the API completely?
34
- container?: HTMLElement;
32
+ ...rest
33
+ }: Omit<SvelteHTMLElements['dialog'], 'children' | 'onclose'> & {
35
34
  /**
36
- * @default 'centered'
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
- layout?: DialogLayout;
43
+ show?: boolean;
39
44
  /**
40
- * index 0 is under 1 is under 2 etc -- the topmost dialog is the last in the array
41
- * @default 0
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
- index?: number;
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
- active?: boolean;
56
+ dismissable?: boolean;
48
57
  /**
49
- * If provided, prevents clicks that would close the dialog
50
- * from bubbling past any elements matching this selector.
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 | null;
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
- children: Snippet<[close: (e?: Event) => void]>;
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
- const ROOT_SELECTOR = 'body'; // TODO make configurable
59
- const CONTAINER_ID = 'fuz_dialog';
60
-
61
- let container_el: HTMLElement | undefined = $state.raw();
62
- $effect(() => {
63
- update_container_el(container);
64
- });
65
-
66
- const update_container_el = (container: HTMLElement | undefined): void => {
67
- if (container) {
68
- container_el = container;
69
- } else {
70
- const found = document.getElementById(CONTAINER_ID);
71
- if (found) {
72
- container_el = found;
73
- } else {
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
- let dialog_el: HTMLElement | undefined = $state.raw();
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
- onclose?.();
109
+ request_close();
92
110
  };
93
111
 
94
- // TODO hook into a ui input system
95
- const on_window_keydown = (e: KeyboardEvent) => {
96
- if (e.key === 'Escape' && !is_editable(e.target)) {
97
- // apply hotkey only for the top-most dialog
98
- const parent_el = dialog_el?.parentElement;
99
- const parents = parent_el?.parentElement?.children;
100
- const index = Array.prototype.indexOf.call(parents, parent_el);
101
- if (!parents || index === parents.length - 1 || index === -1) {
102
- close(e);
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 dialog isn't "ready" until the teleport moves it.
108
- // Rendering the the dialog's children only once it's ready fixes things like `autofocus`.
109
- let ready = $state.raw(false);
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
- <svelte:window onkeydown={active ? on_window_keydown : undefined} />
113
-
114
- <!--
115
- The `tabindex` and `el.focus()` fix scrolling with the keyboard,
116
- needed because SvelteKit puts `tabindex` on the body,
117
- but there's more that needs to be done for accessibility, like focus capture.
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 class="dialog-layout">
140
- <div
141
- class="dialog-wrapper"
142
- role="none"
143
- onmousedown={(e) => {
144
- // Close if clicking outside `content_el` but inside the wrapper
145
- const target = e.target as Element;
146
- if (
147
- content_el &&
148
- (content_el === target ||
149
- !content_el.contains(target) ||
150
- (content_selector && !target.closest(content_selector)))
151
- ) {
152
- close(e);
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
- </div>
164
- </Teleport>
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
- .layout-page .dialog-wrapper {
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>
@@ -1,28 +1,59 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { DialogLayout } from './dialog.js';
3
- type $$ComponentProps = {
4
- container?: HTMLElement;
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
- * @default 'centered'
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
- layout?: DialogLayout;
14
+ show?: boolean;
9
15
  /**
10
- * index 0 is under 1 is under 2 etc -- the topmost dialog is the last in the array
11
- * @default 0
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
- index?: number;
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
- active?: boolean;
27
+ dismissable?: boolean;
18
28
  /**
19
- * If provided, prevents clicks that would close the dialog
20
- * from bubbling past any elements matching this selector.
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 | null;
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
- children: Snippet<[close: (e?: Event) => void]>;
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;AAKpC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AAE7C,KAAK,gBAAgB,GAAI;IAExB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC;CAChD,CAAC;AA4HH,QAAA,MAAM,MAAM,sDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,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>
@@ -0,0 +1,69 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type DialogContext, type DialogCloseButtonAttrs } from './dialog.js';
4
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
5
+ /**
6
+ * The content surface's padding, set as an inline style. Defaults to
7
+ * `var(--space_xl)` for a comfortable gutter between the card edge and its
8
+ * content. Set to `''` or `'0'` for no padding (e.g. a flush image or a
9
+ * surface that manages its own spacing).
10
+ * @default 'var(--space_xl)'
11
+ */
12
+ padding?: string;
13
+ /**
14
+ * Whether to apply the fuz_css `.pane` card class to the content surface --
15
+ * its opaque background, shadow, and rounded corners. `true` (the default) is
16
+ * the standard dialog card; `false` gives a chromeless surface. Either way the
17
+ * surface registers with `Dialog`, so click-outside-to-close keeps working.
18
+ * @default true
19
+ */
20
+ pane?: boolean;
21
+ /**
22
+ * The gutter around the `.pane` card -- its outer margin, and so the area
23
+ * outside the card where a press dismisses the dialog. Set to `''` or `'0'` to remove.
24
+ * @default 'var(--space_xl3)'
25
+ */
26
+ gutter?: string;
27
+ /**
28
+ * The card's max width. The card shrinks to its content and is capped here,
29
+ * so narrow content stays narrow while wide content doesn't sprawl. Set to
30
+ * `''` for no cap (pure shrink-to-content).
31
+ * @default 'var(--distance_md)'
32
+ */
33
+ max_width?: string;
34
+ /**
35
+ * The close button floating just outside the content surface's top-right
36
+ * corner. `true` (the default) renders an absolutely-positioned `.sm.plain.icon_button`
37
+ * that closes the dialog; `false` removes it. It renders after
38
+ * `children`, so a content control (or an `autofocus` element) takes initial
39
+ * focus on open rather than the close button.
40
+ *
41
+ * Pass a `Snippet` to render your own. It receives `attrs`
42
+ * (`DialogCloseButtonAttrs`) carrying the default's placement, styling, a11y,
43
+ * and `onclick` (closes the dialog), plus the `DialogContext` (e.g. `{close}`).
44
+ * Spread `attrs` onto a `<button>` to inherit the corner anchoring and override
45
+ * only what differs (e.g. the glyph), or drop it to place the button freely. The
46
+ * surface is a containing block (`position: relative`), so an absolutely-positioned
47
+ * custom button anchors to it.
48
+ * @default true
49
+ */
50
+ close_button?: boolean | Snippet<[attrs: DialogCloseButtonAttrs, dialog: DialogContext]>;
51
+ /**
52
+ * Rendered inside the content surface. Receives the `DialogContext` (e.g.
53
+ * `{close}`) so content can close the dialog without reaching into `Dialog`'s
54
+ * `children` snippet.
55
+ *
56
+ * The default `close_button` renders as the surface's last child (so a content
57
+ * control or `autofocus` element wins initial focus over it). Because it's a
58
+ * real child, it throws off universal child-spacing selectors: your content's
59
+ * last element is no longer `:last-child` (so `> :not(:last-child)` matches it
60
+ * and `> :last-child` skips it), and `> * + *` matches the button itself. Either
61
+ * scope spacing to your own elements or a class, or wrap your content in a single
62
+ * element so its child selectors no longer see the button.
63
+ */
64
+ children: Snippet<[dialog: DialogContext]>;
65
+ };
66
+ declare const DialogContent: import("svelte").Component<$$ComponentProps, {}, "">;
67
+ type DialogContent = ReturnType<typeof DialogContent>;
68
+ export default DialogContent;
69
+ //# sourceMappingURL=DialogContent.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DialogContent.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/DialogContent.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAC,cAAc,EAAqB,MAAM,iBAAiB,CAAC;AAExE,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,sBAAsB,EAAC,MAAM,aAAa,CAAC;AAE3F,KAAK,gBAAgB,GAAI,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,GAAG;IAC3E;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,KAAK,EAAE,sBAAsB,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzF;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;CAC3C,CAAC;AAoEH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
package/dist/dialog.d.ts CHANGED
@@ -1,24 +1,38 @@
1
- import type { ComponentProps, Component } from 'svelte';
2
- import type Dialog from './Dialog.svelte';
1
+ export type DialogAlign = 'center' | 'top';
2
+ export declare const dialog_aligns: Array<DialogAlign>;
3
3
  /**
4
- * This helper function is needed to construct `DialogParams` with type safety.
5
- * It uses TypeScript's inferred generics for functions,
6
- * which do not work for plain objects as of v5.0.4.
7
- * * `ContextmenuParams` uses a similar strategy.
4
+ * Set by `Dialog`, read by `DialogContent`. Lets the content surface close the
5
+ * dialog without the consumer threading `close` through the `children` snippet.
8
6
  */
9
- export declare const to_dialog_params: <T extends Component<any>>(Component: T, props: ComponentProps<T>, dialog_props?: Partial<ComponentProps<typeof Dialog>>) => DialogParams<T>;
7
+ export interface DialogContext {
8
+ /**
9
+ * Closes the dialog. When passed an event it's swallowed (default-prevented
10
+ * and propagation-stopped) before closing.
11
+ */
12
+ close: (e?: Event) => void;
13
+ /**
14
+ * Registers a content-surface element so a press inside it isn't treated as an
15
+ * outside-dismiss. The surface is known by node identity, independent of any
16
+ * class. Use as an attachment on the surface: `{@attach register_surface}`.
17
+ * Returns a cleanup that unregisters on unmount.
18
+ */
19
+ register_surface: (element: Element) => () => void;
20
+ }
21
+ export declare const dialog_context: {
22
+ get: (error_message?: string) => DialogContext;
23
+ get_maybe: () => DialogContext | undefined;
24
+ set: (value: DialogContext) => DialogContext;
25
+ };
10
26
  /**
11
- * This pattern is based on:
12
- * https://github.com/ivanhofer/sveltekit-typescript-showcase/blob/main/src/01-props/09-svelte-component/Component.svelte
13
- * The main limitation is that the generic cannot be inferred automatically,
14
- * so we use `to_dialog_params` to construct instances in most cases.
15
- * Definining `DialogParams` with no concrete `T` lacks typechecking for `props`.
27
+ * The `attrs` a custom `DialogContent` `close_button` snippet receives.
28
+ * Spread onto a `<button>` to inherit the corner-anchored close button.
16
29
  */
17
- export interface DialogParams<T extends Component<any> = Component<any>> {
18
- Component: T;
19
- props: ComponentProps<T>;
20
- dialog_props?: Partial<ComponentProps<typeof Dialog>> | undefined;
30
+ export interface DialogCloseButtonAttrs {
31
+ type: 'button';
32
+ class: string;
33
+ style: string;
34
+ onclick: DialogContext['close'];
35
+ title: string;
36
+ 'aria-label': string;
21
37
  }
22
- export type DialogLayout = 'centered' | 'page';
23
- export declare const dialog_layouts: Array<DialogLayout>;
24
38
  //# sourceMappingURL=dialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAE,SAAS,EAAC,MAAM,QAAQ,CAAC;AAEtD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,SAAS,CAAC,GAAG,CAAC,EACxD,WAAW,CAAC,EACZ,OAAO,cAAc,CAAC,CAAC,CAAC,EACxB,eAAe,OAAO,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,KACnD,YAAY,CAAC,CAAC,CAIf,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,SAAS,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC;IACtE,SAAS,EAAE,CAAC,CAAC;IACb,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CAClE;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAC/C,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,YAAY,CAAwB,CAAC"}
1
+ {"version":3,"file":"dialog.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/dialog.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,KAAK,CAAC;AAC3C,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,WAAW,CAAqB,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B;;;OAGG;IACH,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC3B;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,cAAc;;;;CAAkC,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACrB"}
package/dist/dialog.js CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * This helper function is needed to construct `DialogParams` with type safety.
3
- * It uses TypeScript's inferred generics for functions,
4
- * which do not work for plain objects as of v5.0.4.
5
- * * `ContextmenuParams` uses a similar strategy.
6
- */
7
- export const to_dialog_params = (Component, props, dialog_props) => ({
8
- Component,
9
- props,
10
- dialog_props,
11
- });
12
- export const dialog_layouts = ['centered', 'page'];
1
+ import { create_context } from './context_helpers.js';
2
+ export const dialog_aligns = ['center', 'top'];
3
+ export const dialog_context = create_context();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.200.0",
3
+ "version": "0.201.0",
4
4
  "description": "Svelte UI library",
5
5
  "tagline": "friendly user zystem",
6
6
  "glyph": "🧶",
@@ -99,7 +99,7 @@
99
99
  "eslint": "^9.39.4",
100
100
  "eslint-plugin-svelte": "^3.19.0",
101
101
  "esm-env": "^1.2.2",
102
- "jsdom": "^27.2.0",
102
+ "jsdom": "^29.1.1",
103
103
  "magic-string": "^0.30.21",
104
104
  "prettier": "^3.7.4",
105
105
  "prettier-plugin-svelte": "^3.5.1",
package/src/lib/dialog.ts CHANGED
@@ -1,35 +1,38 @@
1
- import type {ComponentProps, Component} from 'svelte';
1
+ import {create_context} from './context_helpers.js';
2
2
 
3
- import type Dialog from './Dialog.svelte';
3
+ export type DialogAlign = 'center' | 'top';
4
+ export const dialog_aligns: Array<DialogAlign> = ['center', 'top'];
4
5
 
5
6
  /**
6
- * This helper function is needed to construct `DialogParams` with type safety.
7
- * It uses TypeScript's inferred generics for functions,
8
- * which do not work for plain objects as of v5.0.4.
9
- * * `ContextmenuParams` uses a similar strategy.
7
+ * Set by `Dialog`, read by `DialogContent`. Lets the content surface close the
8
+ * dialog without the consumer threading `close` through the `children` snippet.
10
9
  */
11
- export const to_dialog_params = <T extends Component<any>>(
12
- Component: T,
13
- props: ComponentProps<T>,
14
- dialog_props?: Partial<ComponentProps<typeof Dialog>>,
15
- ): DialogParams<T> => ({
16
- Component,
17
- props,
18
- dialog_props,
19
- });
10
+ export interface DialogContext {
11
+ /**
12
+ * Closes the dialog. When passed an event it's swallowed (default-prevented
13
+ * and propagation-stopped) before closing.
14
+ */
15
+ close: (e?: Event) => void;
16
+ /**
17
+ * Registers a content-surface element so a press inside it isn't treated as an
18
+ * outside-dismiss. The surface is known by node identity, independent of any
19
+ * class. Use as an attachment on the surface: `{@attach register_surface}`.
20
+ * Returns a cleanup that unregisters on unmount.
21
+ */
22
+ register_surface: (element: Element) => () => void;
23
+ }
24
+
25
+ export const dialog_context = create_context<DialogContext>();
20
26
 
21
27
  /**
22
- * This pattern is based on:
23
- * https://github.com/ivanhofer/sveltekit-typescript-showcase/blob/main/src/01-props/09-svelte-component/Component.svelte
24
- * The main limitation is that the generic cannot be inferred automatically,
25
- * so we use `to_dialog_params` to construct instances in most cases.
26
- * Definining `DialogParams` with no concrete `T` lacks typechecking for `props`.
28
+ * The `attrs` a custom `DialogContent` `close_button` snippet receives.
29
+ * Spread onto a `<button>` to inherit the corner-anchored close button.
27
30
  */
28
- export interface DialogParams<T extends Component<any> = Component<any>> {
29
- Component: T;
30
- props: ComponentProps<T>;
31
- dialog_props?: Partial<ComponentProps<typeof Dialog>> | undefined;
31
+ export interface DialogCloseButtonAttrs {
32
+ type: 'button';
33
+ class: string;
34
+ style: string;
35
+ onclick: DialogContext['close'];
36
+ title: string;
37
+ 'aria-label': string;
32
38
  }
33
-
34
- export type DialogLayout = 'centered' | 'page';
35
- export const dialog_layouts: Array<DialogLayout> = ['centered', 'page'];
@@ -1,28 +0,0 @@
1
- <script lang="ts">
2
- import type {Snippet} from 'svelte';
3
-
4
- import type {DialogParams} from './dialog.js';
5
- import Dialog from './Dialog.svelte';
6
-
7
- // TODO this is experimental
8
-
9
- const {
10
- dialogs,
11
- onclose,
12
- children,
13
- }: {
14
- dialogs: Array<DialogParams>;
15
- onclose?: () => void;
16
- children?: Snippet<[dialog: DialogParams]>;
17
- } = $props();
18
- </script>
19
-
20
- {#each dialogs as dialog, index (dialog)}<Dialog
21
- {onclose}
22
- {...dialog.dialog_props}
23
- {index}
24
- active={index === dialogs.length - 1}
25
- >{#if children}{@render children(dialog)}{:else}<dialog.Component
26
- {...dialog.props}
27
- />{/if}</Dialog
28
- >{/each}
@@ -1,11 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { DialogParams } from './dialog.js';
3
- type $$ComponentProps = {
4
- dialogs: Array<DialogParams>;
5
- onclose?: () => void;
6
- children?: Snippet<[dialog: DialogParams]>;
7
- };
8
- declare const Dialogs: import("svelte").Component<$$ComponentProps, {}, "">;
9
- type Dialogs = ReturnType<typeof Dialogs>;
10
- export default Dialogs;
11
- //# sourceMappingURL=Dialogs.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Dialogs.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Dialogs.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AAEpC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AAG7C,KAAK,gBAAgB,GAAI;IACxB,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC3C,CAAC;AAoBH,QAAA,MAAM,OAAO,sDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}