@viniaraujo68/plinth 0.0.0-stage → 0.3.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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -2
  3. package/dist/anchoring.d.ts +61 -0
  4. package/dist/anchoring.js +151 -0
  5. package/dist/async-action.svelte.d.ts +37 -0
  6. package/dist/async-action.svelte.js +64 -0
  7. package/dist/attachments/index.d.ts +2 -0
  8. package/dist/attachments/index.js +2 -0
  9. package/dist/attachments/sortable.d.ts +6 -0
  10. package/dist/attachments/sortable.js +227 -0
  11. package/dist/attachments/tooltip.d.ts +37 -0
  12. package/dist/attachments/tooltip.js +166 -0
  13. package/dist/components/AsyncButton.svelte +49 -0
  14. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  15. package/dist/components/Combobox.svelte +380 -0
  16. package/dist/components/Combobox.svelte.d.ts +74 -0
  17. package/dist/components/Copyable.svelte +116 -0
  18. package/dist/components/Copyable.svelte.d.ts +37 -0
  19. package/dist/components/DateRangePicker.svelte +266 -0
  20. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  21. package/dist/components/Dialog.svelte +102 -0
  22. package/dist/components/Dialog.svelte.d.ts +38 -0
  23. package/dist/components/DragHandle.svelte +26 -0
  24. package/dist/components/DragHandle.svelte.d.ts +7 -0
  25. package/dist/components/Dropdown.svelte +204 -0
  26. package/dist/components/Dropdown.svelte.d.ts +49 -0
  27. package/dist/components/ErrorDisplay.svelte +59 -0
  28. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  29. package/dist/components/ErrorPage.svelte +119 -0
  30. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  31. package/dist/components/JsonInput.svelte +106 -0
  32. package/dist/components/JsonInput.svelte.d.ts +40 -0
  33. package/dist/components/Listbox.svelte +257 -0
  34. package/dist/components/Listbox.svelte.d.ts +78 -0
  35. package/dist/components/LoadingButton.svelte +36 -0
  36. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  37. package/dist/components/Logo.svelte +112 -0
  38. package/dist/components/Logo.svelte.d.ts +29 -0
  39. package/dist/components/Modal.svelte +187 -0
  40. package/dist/components/Modal.svelte.d.ts +62 -0
  41. package/dist/components/MultiSelect.svelte +256 -0
  42. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  43. package/dist/components/OptionPanel.svelte +241 -0
  44. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  45. package/dist/components/Paginator.svelte +205 -0
  46. package/dist/components/Paginator.svelte.d.ts +48 -0
  47. package/dist/components/RefreshButton.svelte +246 -0
  48. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  49. package/dist/components/SegmentedControl.svelte +147 -0
  50. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  51. package/dist/components/Select.svelte +312 -0
  52. package/dist/components/Select.svelte.d.ts +68 -0
  53. package/dist/components/Skeleton.svelte +97 -0
  54. package/dist/components/Skeleton.svelte.d.ts +32 -0
  55. package/dist/components/Tooltip.svelte +60 -0
  56. package/dist/components/Tooltip.svelte.d.ts +58 -0
  57. package/dist/components/date-range.d.ts +101 -0
  58. package/dist/components/date-range.js +144 -0
  59. package/dist/components/index.d.ts +26 -0
  60. package/dist/components/index.js +38 -0
  61. package/dist/components/json-text.d.ts +22 -0
  62. package/dist/components/json-text.js +22 -0
  63. package/dist/components/pick.d.ts +18 -0
  64. package/dist/components/pick.js +18 -0
  65. package/dist/components/segmented.d.ts +7 -0
  66. package/dist/components/segmented.js +1 -0
  67. package/dist/components/select.d.ts +66 -0
  68. package/dist/components/select.js +90 -0
  69. package/dist/confirm/Confirmer.svelte +194 -0
  70. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  71. package/dist/confirm/challenge.d.ts +16 -0
  72. package/dist/confirm/challenge.js +16 -0
  73. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  74. package/dist/confirm/confirm-manager.svelte.js +149 -0
  75. package/dist/confirm/index.d.ts +3 -0
  76. package/dist/confirm/index.js +3 -0
  77. package/dist/error.d.ts +25 -0
  78. package/dist/error.js +39 -0
  79. package/dist/formatters.d.ts +95 -0
  80. package/dist/formatters.js +146 -0
  81. package/dist/http.d.ts +68 -0
  82. package/dist/http.js +138 -0
  83. package/dist/index.d.ts +10 -0
  84. package/dist/index.js +14 -0
  85. package/dist/routing.svelte.d.ts +169 -0
  86. package/dist/routing.svelte.js +208 -0
  87. package/dist/shell/AppShell.svelte +874 -0
  88. package/dist/shell/AppShell.svelte.d.ts +93 -0
  89. package/dist/shell/Breadcrumbs.svelte +78 -0
  90. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  91. package/dist/shell/crumbs.d.ts +23 -0
  92. package/dist/shell/crumbs.js +1 -0
  93. package/dist/shell/index.d.ts +3 -0
  94. package/dist/shell/index.js +6 -0
  95. package/dist/table/ColumnMenu.svelte +249 -0
  96. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  97. package/dist/table/DataTable.svelte +643 -0
  98. package/dist/table/DataTable.svelte.d.ts +153 -0
  99. package/dist/table/columns.d.ts +55 -0
  100. package/dist/table/columns.js +153 -0
  101. package/dist/table/index.d.ts +4 -0
  102. package/dist/table/index.js +8 -0
  103. package/dist/table/sort.d.ts +48 -0
  104. package/dist/table/sort.js +139 -0
  105. package/dist/table/types.d.ts +93 -0
  106. package/dist/table/types.js +1 -0
  107. package/dist/theme/ThemeController.svelte +54 -0
  108. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  109. package/dist/theme/ThemeToggle.svelte +119 -0
  110. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  111. package/dist/theme/css-color.d.ts +23 -0
  112. package/dist/theme/css-color.js +31 -0
  113. package/dist/theme/index.d.ts +4 -0
  114. package/dist/theme/index.js +5 -0
  115. package/dist/theme/theme.css +410 -0
  116. package/dist/theme/theme.svelte.d.ts +54 -0
  117. package/dist/theme/theme.svelte.js +58 -0
  118. package/dist/toast/Toaster.svelte +257 -0
  119. package/dist/toast/Toaster.svelte.d.ts +35 -0
  120. package/dist/toast/index.d.ts +2 -0
  121. package/dist/toast/index.js +2 -0
  122. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  123. package/dist/toast/toast-manager.svelte.js +176 -0
  124. package/dist/user/ScopedComponent.svelte +47 -0
  125. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  126. package/dist/user/anonymous.d.ts +21 -0
  127. package/dist/user/anonymous.js +26 -0
  128. package/dist/user/context.d.ts +55 -0
  129. package/dist/user/context.js +13 -0
  130. package/dist/user/index.d.ts +3 -0
  131. package/dist/user/index.js +6 -0
  132. package/package.json +133 -4
@@ -0,0 +1,266 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import type { ClassValue } from "svelte/elements";
4
+ import {
5
+ anchorSelection,
6
+ buildRange,
7
+ createReAnchor,
8
+ CUSTOM_PRESET_ID,
9
+ DEFAULT_PRESETS,
10
+ isoToLocalInput,
11
+ isValidRange,
12
+ localInputToIso,
13
+ type DateRange,
14
+ type DateRangePreset,
15
+ } from "./date-range.js";
16
+
17
+ interface Props {
18
+ class?: ClassValue;
19
+ /**
20
+ * The selected window, as UTC ISO instants. Bindable, and also reported through
21
+ * {@link Props.onchange} — a consumer needs one or the other, never both.
22
+ *
23
+ * It is an output, not an input: a relative preset overwrites it on every anchor. Passing one
24
+ * in only seeds the picker, and only alongside `preset="custom"`.
25
+ */
26
+ value?: DateRange;
27
+ /** The buttons offered. `id` may not be `"custom"`, which the picker reserves. */
28
+ presets?: readonly DateRangePreset[];
29
+ /**
30
+ * Which button starts selected — a preset id, or `"custom"`. Defaults to the first preset, or
31
+ * to custom when `value` was seeded without naming a preset.
32
+ */
33
+ preset?: string;
34
+ /**
35
+ * How often a relative range slides forward, in milliseconds. `0` turns re-anchoring off.
36
+ *
37
+ * A dashboard left open overnight should still be showing the last 24 hours in the morning,
38
+ * which is the whole reason this is on by default.
39
+ */
40
+ reAnchorMs?: number;
41
+ /** Names the preset group for assistive technology. */
42
+ label?: string;
43
+ /**
44
+ * Label of the button that switches to the typed pair. The id behind it stays
45
+ * {@link CUSTOM_PRESET_ID} whatever this says, so `preset="custom"` keeps working.
46
+ */
47
+ customLabel?: string;
48
+ /** Label of the start field. */
49
+ fromLabel?: string;
50
+ /** Label of the end field. */
51
+ toLabel?: string;
52
+ /** Shown, as an alert, while the typed pair is out of order. */
53
+ invalidRangeLabel?: string;
54
+ onchange?: (value: DateRange) => void;
55
+ }
56
+
57
+ let {
58
+ class: cls,
59
+ value = $bindable(),
60
+ presets = DEFAULT_PRESETS,
61
+ preset,
62
+ reAnchorMs = 60_000,
63
+ label = "Date range",
64
+ customLabel = "Custom",
65
+ fromLabel = "From",
66
+ toLabel = "To",
67
+ invalidRangeLabel = "The start must not be after the end.",
68
+ onchange,
69
+ }: Props = $props();
70
+
71
+ const uid = $props.id();
72
+ const fromId = `plinth-range-from-${uid}`;
73
+ const toId = `plinth-range-to-${uid}`;
74
+ const errorId = `plinth-range-error-${uid}`;
75
+
76
+ // The selection is the picker's own state rather than a bound prop: the pair is the entire
77
+ // output, and a second bound value would be a second thing for a consumer to keep in step.
78
+ let selection = $state(
79
+ untrack(() => preset ?? (value ? CUSTOM_PRESET_ID : (presets[0]?.id ?? CUSTOM_PRESET_ID))),
80
+ );
81
+
82
+ // Wall-clock text for the two fields. Empty until custom mode is entered, because reading the
83
+ // local zone is a client-only measurement -- seeding these at init would make a prerendered
84
+ // page paint the build machine's offset and then correct itself on hydration.
85
+ let customFrom = $state("");
86
+ let customTo = $state("");
87
+
88
+ const isCustom = $derived(selection === CUSTOM_PRESET_ID);
89
+
90
+ const emit = (next: DateRange) => {
91
+ if (value?.from === next.from && value?.to === next.to) return;
92
+
93
+ value = next;
94
+ onchange?.(next);
95
+ };
96
+
97
+ const anchor = () => {
98
+ const next = anchorSelection(selection, presets, Date.now());
99
+ if (next) emit(next);
100
+ };
101
+
102
+ const seedCustomFields = (range: DateRange | undefined) => {
103
+ customFrom = isoToLocalInput(range?.from) ?? "";
104
+ customTo = isoToLocalInput(range?.to) ?? "";
105
+ };
106
+
107
+ const enterCustom = () => {
108
+ const current = value ?? (presets[0] ? buildRange(presets[0]) : undefined);
109
+
110
+ seedCustomFields(current);
111
+ selection = CUSTOM_PRESET_ID;
112
+
113
+ // The fields only hold whole minutes, so the range they now show is not quite the one that
114
+ // was emitted. Re-emitting the truncated pair is what keeps the value and the two fields the
115
+ // user is about to edit from disagreeing by up to a minute.
116
+ commitCustom();
117
+ };
118
+
119
+ const commitCustom = () => {
120
+ const from = localInputToIso(customFrom);
121
+ const to = localInputToIso(customTo);
122
+
123
+ if (from === null || to === null || !isValidRange(from, to)) return;
124
+
125
+ emit({ from, to });
126
+ };
127
+
128
+ const rangeError = $derived.by(() => {
129
+ if (!isCustom) return null;
130
+
131
+ const from = localInputToIso(customFrom);
132
+ const to = localInputToIso(customTo);
133
+
134
+ // A half-typed field is not yet wrong, so it says nothing rather than flashing an error at
135
+ // someone who is still filling the form in.
136
+ if (from === null || to === null) return null;
137
+
138
+ return isValidRange(from, to) ? null : invalidRangeLabel;
139
+ });
140
+
141
+ $effect(() => {
142
+ const intervalMs = reAnchorMs;
143
+ if (selection === CUSTOM_PRESET_ID) return;
144
+
145
+ // `presets` is read inside the anchor, untracked: a consumer who passes an array literal
146
+ // hands over a new identity on every render, and tracking it would turn each emit into
147
+ // another render into another anchor.
148
+ untrack(anchor);
149
+ if (intervalMs <= 0) return;
150
+
151
+ const clock = createReAnchor({ intervalMs, onAnchor: () => untrack(anchor) });
152
+ const onVisibilityChange = () => clock.setHidden(document.hidden);
153
+
154
+ clock.setHidden(document.hidden);
155
+ clock.start();
156
+ document.addEventListener("visibilitychange", onVisibilityChange);
157
+
158
+ return () => {
159
+ clock.stop();
160
+ document.removeEventListener("visibilitychange", onVisibilityChange);
161
+ };
162
+ });
163
+
164
+ $effect(() => {
165
+ if (selection !== CUSTOM_PRESET_ID) return;
166
+
167
+ // Covers the one path into custom mode that is not a click: a picker mounted with
168
+ // `preset="custom"`. Seeding without emitting, because a value the consumer supplied is
169
+ // theirs to keep -- the click path is the one that re-emits.
170
+ untrack(() => {
171
+ if (!customFrom && !customTo) seedCustomFields(value);
172
+ });
173
+ });
174
+ </script>
175
+
176
+ <!--
177
+ @component
178
+ A from/to picker built as a plain form control: relative presets, a custom pair of local
179
+ date-times, and `{ from, to }` as UTC ISO strings on the way out. It fetches nothing, knows no
180
+ query parameter, and reads no URL — what the pair means is the consumer's business.
181
+
182
+ A relative preset re-anchors on a timer (`reAnchorMs`, 60s by default, `0` to disable), so a
183
+ dashboard left open keeps sliding forward. The timer suspends while the tab is hidden and fires
184
+ once on return. A custom range is never touched by it.
185
+
186
+ The two custom fields are `<input type="datetime-local">`, which means they are wall-clock time in
187
+ the viewer's own zone with no offset attached, while the emitted pair stays in UTC. The conversion
188
+ runs to whole minutes in both directions; see `isoToLocalInput` / `localInputToIso`.
189
+
190
+ Every word it renders is a prop with an English default — `label`, `customLabel`, `fromLabel`,
191
+ `toLabel` and `invalidRangeLabel` — because the library ships no translations. Only the wording
192
+ moves: the custom button keeps the reserved `"custom"` id whatever `customLabel` says. The preset
193
+ buttons are labelled by the list itself, through `DateRangePreset.label`.
194
+
195
+ ```svelte
196
+ <DateRangePicker bind:value={range} preset="24h" onchange={(r) => load(r)} />
197
+ ```
198
+ -->
199
+
200
+ <div class={["flex flex-wrap items-end gap-x-4 gap-y-3", cls]}>
201
+ <div class="join" role="group" aria-label={label}>
202
+ {#each presets as option (option.id)}
203
+ <button
204
+ type="button"
205
+ class="btn join-item min-h-11"
206
+ class:btn-primary={selection === option.id}
207
+ aria-pressed={selection === option.id}
208
+ onclick={() => (selection = option.id)}
209
+ >
210
+ {option.label}
211
+ </button>
212
+ {/each}
213
+
214
+ <button
215
+ type="button"
216
+ class="btn join-item min-h-11"
217
+ class:btn-primary={isCustom}
218
+ aria-pressed={isCustom}
219
+ onclick={enterCustom}
220
+ >
221
+ {customLabel}
222
+ </button>
223
+ </div>
224
+
225
+ {#if isCustom}
226
+ <div class="flex flex-wrap items-end gap-x-4 gap-y-3">
227
+ <div class="flex flex-col gap-1">
228
+ <label class="label text-xs" for={fromId}>{fromLabel}</label>
229
+ <input
230
+ id={fromId}
231
+ class="input min-h-11"
232
+ class:input-error={rangeError !== null}
233
+ type="datetime-local"
234
+ value={customFrom}
235
+ aria-invalid={rangeError !== null}
236
+ aria-describedby={rangeError === null ? undefined : errorId}
237
+ oninput={({ currentTarget }) => {
238
+ customFrom = currentTarget.value;
239
+ commitCustom();
240
+ }}
241
+ />
242
+ </div>
243
+
244
+ <div class="flex flex-col gap-1">
245
+ <label class="label text-xs" for={toId}>{toLabel}</label>
246
+ <input
247
+ id={toId}
248
+ class="input min-h-11"
249
+ class:input-error={rangeError !== null}
250
+ type="datetime-local"
251
+ value={customTo}
252
+ aria-invalid={rangeError !== null}
253
+ aria-describedby={rangeError === null ? undefined : errorId}
254
+ oninput={({ currentTarget }) => {
255
+ customTo = currentTarget.value;
256
+ commitCustom();
257
+ }}
258
+ />
259
+ </div>
260
+ </div>
261
+ {/if}
262
+
263
+ {#if rangeError !== null}
264
+ <p id={errorId} class="basis-full text-sm text-error" role="alert">{rangeError}</p>
265
+ {/if}
266
+ </div>
@@ -0,0 +1,66 @@
1
+ import type { ClassValue } from "svelte/elements";
2
+ import { type DateRange, type DateRangePreset } from "./date-range.js";
3
+ interface Props {
4
+ class?: ClassValue;
5
+ /**
6
+ * The selected window, as UTC ISO instants. Bindable, and also reported through
7
+ * {@link Props.onchange} — a consumer needs one or the other, never both.
8
+ *
9
+ * It is an output, not an input: a relative preset overwrites it on every anchor. Passing one
10
+ * in only seeds the picker, and only alongside `preset="custom"`.
11
+ */
12
+ value?: DateRange;
13
+ /** The buttons offered. `id` may not be `"custom"`, which the picker reserves. */
14
+ presets?: readonly DateRangePreset[];
15
+ /**
16
+ * Which button starts selected — a preset id, or `"custom"`. Defaults to the first preset, or
17
+ * to custom when `value` was seeded without naming a preset.
18
+ */
19
+ preset?: string;
20
+ /**
21
+ * How often a relative range slides forward, in milliseconds. `0` turns re-anchoring off.
22
+ *
23
+ * A dashboard left open overnight should still be showing the last 24 hours in the morning,
24
+ * which is the whole reason this is on by default.
25
+ */
26
+ reAnchorMs?: number;
27
+ /** Names the preset group for assistive technology. */
28
+ label?: string;
29
+ /**
30
+ * Label of the button that switches to the typed pair. The id behind it stays
31
+ * {@link CUSTOM_PRESET_ID} whatever this says, so `preset="custom"` keeps working.
32
+ */
33
+ customLabel?: string;
34
+ /** Label of the start field. */
35
+ fromLabel?: string;
36
+ /** Label of the end field. */
37
+ toLabel?: string;
38
+ /** Shown, as an alert, while the typed pair is out of order. */
39
+ invalidRangeLabel?: string;
40
+ onchange?: (value: DateRange) => void;
41
+ }
42
+ /**
43
+ * A from/to picker built as a plain form control: relative presets, a custom pair of local
44
+ * date-times, and `{ from, to }` as UTC ISO strings on the way out. It fetches nothing, knows no
45
+ * query parameter, and reads no URL — what the pair means is the consumer's business.
46
+ *
47
+ * A relative preset re-anchors on a timer (`reAnchorMs`, 60s by default, `0` to disable), so a
48
+ * dashboard left open keeps sliding forward. The timer suspends while the tab is hidden and fires
49
+ * once on return. A custom range is never touched by it.
50
+ *
51
+ * The two custom fields are `<input type="datetime-local">`, which means they are wall-clock time in
52
+ * the viewer's own zone with no offset attached, while the emitted pair stays in UTC. The conversion
53
+ * runs to whole minutes in both directions; see `isoToLocalInput` / `localInputToIso`.
54
+ *
55
+ * Every word it renders is a prop with an English default — `label`, `customLabel`, `fromLabel`,
56
+ * `toLabel` and `invalidRangeLabel` — because the library ships no translations. Only the wording
57
+ * moves: the custom button keeps the reserved `"custom"` id whatever `customLabel` says. The preset
58
+ * buttons are labelled by the list itself, through `DateRangePreset.label`.
59
+ *
60
+ * ```svelte
61
+ * <DateRangePicker bind:value={range} preset="24h" onchange={(r) => load(r)} />
62
+ * ```
63
+ */
64
+ declare const DateRangePicker: import("svelte").Component<Props, {}, "value">;
65
+ type DateRangePicker = ReturnType<typeof DateRangePicker>;
66
+ export default DateRangePicker;
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ import { flushSync, type Snippet } from "svelte";
3
+ import type { Attachment } from "svelte/attachments";
4
+ import type { HTMLDialogAttributes } from "svelte/elements";
5
+
6
+ interface Props extends HTMLDialogAttributes {
7
+ /** Rendered only while the dialog is on screen. */
8
+ children: Snippet;
9
+ }
10
+
11
+ let { children, ...rest }: Props = $props();
12
+
13
+ let mounted = $state(false);
14
+ let element = $state<HTMLDialogElement>();
15
+
16
+ /**
17
+ * Mount the content and raise the dialog into the top layer as a modal.
18
+ *
19
+ * Safe to call on an already open dialog, which `showModal()` itself is not.
20
+ */
21
+ export function show() {
22
+ mounted = true;
23
+ // The element does not exist until the block below has been applied to the DOM. Flushing here
24
+ // is what keeps `show()` a plain synchronous call: without it the caller would have to await a
25
+ // tick before the dialog it just asked for exists.
26
+ flushSync();
27
+ if (element && !element.open) element.showModal();
28
+ }
29
+
30
+ /** Close the dialog. The content stays mounted until the element has finished disappearing. */
31
+ export function close(returnValue?: string) {
32
+ element?.close(returnValue);
33
+ }
34
+
35
+ const unmountWhenHidden: Attachment<HTMLDialogElement> = (dialog) => {
36
+ const listener = (event: Event) => {
37
+ // Opening transitions too, and the `toggle` that announces the open state arrives while the
38
+ // element is still being raised. Only an end state that is actually invisible may unmount.
39
+ if (event instanceof ToggleEvent && event.newState === "open") return;
40
+
41
+ // Being closed and being gone are not the same thing. A stylesheet that keeps the element
42
+ // displayed while fading it out -- daisyUI's `.modal` is `display: grid` at all times and
43
+ // animates `visibility` -- would otherwise have the content ripped out mid-animation, so the
44
+ // question asked here is "has it finished disappearing", not "is the open attribute gone".
45
+ if (mounted && !dialog.checkVisibility({ opacityProperty: true, visibilityProperty: true }))
46
+ mounted = false;
47
+ };
48
+
49
+ // `toggle` covers an instant hide (no transition, or reduced motion); the two transition events
50
+ // cover a fade, including one cut short by a reopen.
51
+ dialog.addEventListener("toggle", listener);
52
+ dialog.addEventListener("transitionend", listener);
53
+ dialog.addEventListener("transitioncancel", listener);
54
+
55
+ return () => {
56
+ dialog.removeEventListener("toggle", listener);
57
+ dialog.removeEventListener("transitionend", listener);
58
+ dialog.removeEventListener("transitioncancel", listener);
59
+ };
60
+ };
61
+
62
+ // The element's own `open` attribute is the other way in, and it shows the dialog NON-modally:
63
+ // no top layer, no inert background, no Escape. Seeding the mount from it keeps that path from
64
+ // rendering an empty shell.
65
+ $effect(() => {
66
+ if (rest.open) mounted = true;
67
+ });
68
+ </script>
69
+
70
+ <!--
71
+ @component
72
+ A native `<dialog>` that only exists while it is on screen.
73
+
74
+ The point of the wrapper is what it does NOT do. Modality, the focus trap, Escape, returning focus
75
+ to whatever opened it and stacking above the rest of the page without a z-index fight are all the
76
+ browser's, delivered by `showModal()` and correct in a way a hand-rolled overlay rarely is. This
77
+ component adds one thing on top: the content is mounted on `show()` and unmounted once the element
78
+ has finished disappearing, so a page can declare a dozen dialogs -- each with its own form, fetch
79
+ or chart inside -- and pay for none of them until one is opened.
80
+
81
+ There is no chrome here: no title, no close button, no padding. Use `Modal` for that, or style the
82
+ element yourself through `class` and any other `<dialog>` attribute, which are forwarded.
83
+
84
+ ```svelte
85
+ <script lang="ts">
86
+ let confirmation = $state<Dialog>();
87
+ </script>
88
+
89
+ <button onclick={() => confirmation?.show()}>Delete</button>
90
+
91
+ <Dialog bind:this={confirmation} class="rounded-box bg-base-100 p-6">
92
+ <p>This cannot be undone.</p>
93
+ <button onclick={() => confirmation?.close()}>Cancel</button>
94
+ </Dialog>
95
+ ```
96
+ -->
97
+
98
+ {#if mounted}
99
+ <dialog bind:this={element} {@attach unmountWhenHidden} {...rest}>
100
+ {@render children()}
101
+ </dialog>
102
+ {/if}
@@ -0,0 +1,38 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { HTMLDialogAttributes } from "svelte/elements";
3
+ interface Props extends HTMLDialogAttributes {
4
+ /** Rendered only while the dialog is on screen. */
5
+ children: Snippet;
6
+ }
7
+ /**
8
+ * A native `<dialog>` that only exists while it is on screen.
9
+ *
10
+ * The point of the wrapper is what it does NOT do. Modality, the focus trap, Escape, returning focus
11
+ * to whatever opened it and stacking above the rest of the page without a z-index fight are all the
12
+ * browser's, delivered by `showModal()` and correct in a way a hand-rolled overlay rarely is. This
13
+ * component adds one thing on top: the content is mounted on `show()` and unmounted once the element
14
+ * has finished disappearing, so a page can declare a dozen dialogs -- each with its own form, fetch
15
+ * or chart inside -- and pay for none of them until one is opened.
16
+ *
17
+ * There is no chrome here: no title, no close button, no padding. Use `Modal` for that, or style the
18
+ * element yourself through `class` and any other `<dialog>` attribute, which are forwarded.
19
+ *
20
+ * ```svelte
21
+ * <script lang="ts">
22
+ * let confirmation = $state<Dialog>();
23
+ * </script>
24
+ *
25
+ * <button onclick={() => confirmation?.show()}>Delete</button>
26
+ *
27
+ * <Dialog bind:this={confirmation} class="rounded-box bg-base-100 p-6">
28
+ * <p>This cannot be undone.</p>
29
+ * <button onclick={() => confirmation?.close()}>Cancel</button>
30
+ * </Dialog>
31
+ * ```
32
+ */
33
+ declare const Dialog: import("svelte").Component<Props, {
34
+ show: () => void;
35
+ close: (returnValue?: string) => void;
36
+ }, "">;
37
+ type Dialog = ReturnType<typeof Dialog>;
38
+ export default Dialog;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+
4
+ interface Props extends Omit<HTMLButtonAttributes, "type" | "children"> {
5
+ label: string;
6
+ }
7
+
8
+ let { label, class: className = "", ...rest }: Props = $props();
9
+ </script>
10
+
11
+ <button
12
+ {...rest}
13
+ type="button"
14
+ aria-label={label}
15
+ data-sortable-handle
16
+ class="inline-grid shrink-0 cursor-grab touch-none place-items-center self-stretch px-1 text-base-content/45 select-none hover:text-base-content focus-visible:text-base-content focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary in-data-sortable-dragging:cursor-grabbing {className}"
17
+ >
18
+ <svg viewBox="0 0 10 15" class="h-[15px] w-2.5" fill="currentColor" aria-hidden="true">
19
+ <circle cx="2.5" cy="2.5" r="1.5" />
20
+ <circle cx="7.5" cy="2.5" r="1.5" />
21
+ <circle cx="2.5" cy="7.5" r="1.5" />
22
+ <circle cx="7.5" cy="7.5" r="1.5" />
23
+ <circle cx="2.5" cy="12.5" r="1.5" />
24
+ <circle cx="7.5" cy="12.5" r="1.5" />
25
+ </svg>
26
+ </button>
@@ -0,0 +1,7 @@
1
+ import type { HTMLButtonAttributes } from "svelte/elements";
2
+ interface Props extends Omit<HTMLButtonAttributes, "type" | "children"> {
3
+ label: string;
4
+ }
5
+ declare const DragHandle: import("svelte").Component<Props, {}, "">;
6
+ type DragHandle = ReturnType<typeof DragHandle>;
7
+ export default DragHandle;