@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.
- package/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/anchoring.d.ts +61 -0
- package/dist/anchoring.js +151 -0
- package/dist/async-action.svelte.d.ts +37 -0
- package/dist/async-action.svelte.js +64 -0
- package/dist/attachments/index.d.ts +2 -0
- package/dist/attachments/index.js +2 -0
- package/dist/attachments/sortable.d.ts +6 -0
- package/dist/attachments/sortable.js +227 -0
- package/dist/attachments/tooltip.d.ts +37 -0
- package/dist/attachments/tooltip.js +166 -0
- package/dist/components/AsyncButton.svelte +49 -0
- package/dist/components/AsyncButton.svelte.d.ts +32 -0
- package/dist/components/Combobox.svelte +380 -0
- package/dist/components/Combobox.svelte.d.ts +74 -0
- package/dist/components/Copyable.svelte +116 -0
- package/dist/components/Copyable.svelte.d.ts +37 -0
- package/dist/components/DateRangePicker.svelte +266 -0
- package/dist/components/DateRangePicker.svelte.d.ts +66 -0
- package/dist/components/Dialog.svelte +102 -0
- package/dist/components/Dialog.svelte.d.ts +38 -0
- package/dist/components/DragHandle.svelte +26 -0
- package/dist/components/DragHandle.svelte.d.ts +7 -0
- package/dist/components/Dropdown.svelte +204 -0
- package/dist/components/Dropdown.svelte.d.ts +49 -0
- package/dist/components/ErrorDisplay.svelte +59 -0
- package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
- package/dist/components/ErrorPage.svelte +119 -0
- package/dist/components/ErrorPage.svelte.d.ts +52 -0
- package/dist/components/JsonInput.svelte +106 -0
- package/dist/components/JsonInput.svelte.d.ts +40 -0
- package/dist/components/Listbox.svelte +257 -0
- package/dist/components/Listbox.svelte.d.ts +78 -0
- package/dist/components/LoadingButton.svelte +36 -0
- package/dist/components/LoadingButton.svelte.d.ts +22 -0
- package/dist/components/Logo.svelte +112 -0
- package/dist/components/Logo.svelte.d.ts +29 -0
- package/dist/components/Modal.svelte +187 -0
- package/dist/components/Modal.svelte.d.ts +62 -0
- package/dist/components/MultiSelect.svelte +256 -0
- package/dist/components/MultiSelect.svelte.d.ts +89 -0
- package/dist/components/OptionPanel.svelte +241 -0
- package/dist/components/OptionPanel.svelte.d.ts +51 -0
- package/dist/components/Paginator.svelte +205 -0
- package/dist/components/Paginator.svelte.d.ts +48 -0
- package/dist/components/RefreshButton.svelte +246 -0
- package/dist/components/RefreshButton.svelte.d.ts +46 -0
- package/dist/components/SegmentedControl.svelte +147 -0
- package/dist/components/SegmentedControl.svelte.d.ts +71 -0
- package/dist/components/Select.svelte +312 -0
- package/dist/components/Select.svelte.d.ts +68 -0
- package/dist/components/Skeleton.svelte +97 -0
- package/dist/components/Skeleton.svelte.d.ts +32 -0
- package/dist/components/Tooltip.svelte +60 -0
- package/dist/components/Tooltip.svelte.d.ts +58 -0
- package/dist/components/date-range.d.ts +101 -0
- package/dist/components/date-range.js +144 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.js +38 -0
- package/dist/components/json-text.d.ts +22 -0
- package/dist/components/json-text.js +22 -0
- package/dist/components/pick.d.ts +18 -0
- package/dist/components/pick.js +18 -0
- package/dist/components/segmented.d.ts +7 -0
- package/dist/components/segmented.js +1 -0
- package/dist/components/select.d.ts +66 -0
- package/dist/components/select.js +90 -0
- package/dist/confirm/Confirmer.svelte +194 -0
- package/dist/confirm/Confirmer.svelte.d.ts +45 -0
- package/dist/confirm/challenge.d.ts +16 -0
- package/dist/confirm/challenge.js +16 -0
- package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
- package/dist/confirm/confirm-manager.svelte.js +149 -0
- package/dist/confirm/index.d.ts +3 -0
- package/dist/confirm/index.js +3 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.js +39 -0
- package/dist/formatters.d.ts +95 -0
- package/dist/formatters.js +146 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.js +138 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/routing.svelte.d.ts +169 -0
- package/dist/routing.svelte.js +208 -0
- package/dist/shell/AppShell.svelte +874 -0
- package/dist/shell/AppShell.svelte.d.ts +93 -0
- package/dist/shell/Breadcrumbs.svelte +78 -0
- package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
- package/dist/shell/crumbs.d.ts +23 -0
- package/dist/shell/crumbs.js +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +6 -0
- package/dist/table/ColumnMenu.svelte +249 -0
- package/dist/table/ColumnMenu.svelte.d.ts +47 -0
- package/dist/table/DataTable.svelte +643 -0
- package/dist/table/DataTable.svelte.d.ts +153 -0
- package/dist/table/columns.d.ts +55 -0
- package/dist/table/columns.js +153 -0
- package/dist/table/index.d.ts +4 -0
- package/dist/table/index.js +8 -0
- package/dist/table/sort.d.ts +48 -0
- package/dist/table/sort.js +139 -0
- package/dist/table/types.d.ts +93 -0
- package/dist/table/types.js +1 -0
- package/dist/theme/ThemeController.svelte +54 -0
- package/dist/theme/ThemeController.svelte.d.ts +27 -0
- package/dist/theme/ThemeToggle.svelte +119 -0
- package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
- package/dist/theme/css-color.d.ts +23 -0
- package/dist/theme/css-color.js +31 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/theme.css +410 -0
- package/dist/theme/theme.svelte.d.ts +54 -0
- package/dist/theme/theme.svelte.js +58 -0
- package/dist/toast/Toaster.svelte +257 -0
- package/dist/toast/Toaster.svelte.d.ts +35 -0
- package/dist/toast/index.d.ts +2 -0
- package/dist/toast/index.js +2 -0
- package/dist/toast/toast-manager.svelte.d.ts +99 -0
- package/dist/toast/toast-manager.svelte.js +176 -0
- package/dist/user/ScopedComponent.svelte +47 -0
- package/dist/user/ScopedComponent.svelte.d.ts +29 -0
- package/dist/user/anonymous.d.ts +21 -0
- package/dist/user/anonymous.js +26 -0
- package/dist/user/context.d.ts +55 -0
- package/dist/user/context.js +13 -0
- package/dist/user/index.d.ts +3 -0
- package/dist/user/index.js +6 -0
- 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;
|