@viniaraujo68/plinth 0.5.0 → 0.6.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 +8 -0
- package/dist/components/DateRangePicker.svelte +93 -57
- package/dist/components/DateRangePicker.svelte.d.ts +32 -15
- package/dist/components/date-range.d.ts +82 -14
- package/dist/components/date-range.js +86 -5
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -315,6 +315,14 @@ instead of wrapping, fading at the edge while tabs are hidden past it. `value` i
|
|
|
315
315
|
that keeps its section in the URL passes it in and navigates on `onchange`, and `idPrefix` makes the
|
|
316
316
|
tab ids predictable so the panel can name its tab through `aria-labelledby`.
|
|
317
317
|
|
|
318
|
+
`DateRangePicker` picks a from/to pair from presets or two typed fields. By default it is a window
|
|
319
|
+
of instants — presets are durations back from now ("24h"), the fields are local date-times and the
|
|
320
|
+
pair goes out as UTC ISO strings. With `mode="date"` it is a span of calendar days: the fields are
|
|
321
|
+
date fields, the pair is `YYYY-MM-DD`, and an empty end leaves that side open, so "since March" and
|
|
322
|
+
"all time" are pairs too. Presets can follow the calendar instead of a duration — `thisMonth`,
|
|
323
|
+
`lastMonth`, `lastMonths(n)` and `allTime` build the usual ones, and any `range(now)` function will
|
|
324
|
+
do. Every preset re-anchors on a timer, and a picker handed a pair opens on the preset that names it.
|
|
325
|
+
|
|
318
326
|
`JsonInput` edits a JSON document as text: it reports through a bindable `invalid` whether the text
|
|
319
327
|
parses, shows the parser's message after the first blur, can reindent on blur, and sets the
|
|
320
328
|
textarea's custom validity so a form will not submit a broken document. `parseJsonText` and
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from "svelte";
|
|
3
3
|
import type { ClassValue } from "svelte/elements";
|
|
4
|
+
import SegmentedControl from "./SegmentedControl.svelte";
|
|
4
5
|
import {
|
|
5
6
|
anchorSelection,
|
|
6
7
|
buildRange,
|
|
@@ -8,27 +9,38 @@
|
|
|
8
9
|
CUSTOM_PRESET_ID,
|
|
9
10
|
DEFAULT_PRESETS,
|
|
10
11
|
isoToLocalInput,
|
|
12
|
+
isValidDayRange,
|
|
11
13
|
isValidRange,
|
|
12
14
|
localInputToIso,
|
|
15
|
+
parseDay,
|
|
16
|
+
selectionFor,
|
|
13
17
|
type DateRange,
|
|
18
|
+
type DateRangeMode,
|
|
14
19
|
type DateRangePreset,
|
|
15
20
|
} from "./date-range.js";
|
|
16
21
|
|
|
17
22
|
interface Props {
|
|
18
23
|
class?: ClassValue;
|
|
19
24
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
25
|
+
* `"datetime"` (the default) picks a window of instants, emitted as UTC ISO strings, with
|
|
26
|
+
* date-and-time fields. `"date"` picks a span of calendar days, emitted as `YYYY-MM-DD`, with
|
|
27
|
+
* date fields either of which may stay empty to leave that side open.
|
|
28
|
+
*/
|
|
29
|
+
mode?: DateRangeMode;
|
|
30
|
+
/**
|
|
31
|
+
* The selected window — UTC ISO instants, or days in `"date"` mode. Bindable, and also reported
|
|
32
|
+
* through {@link Props.onchange} — a consumer needs one or the other, never both.
|
|
22
33
|
*
|
|
23
34
|
* It is an output, not an input: a relative preset overwrites it on every anchor. Passing one
|
|
24
|
-
* in only seeds the picker
|
|
35
|
+
* in only seeds the picker: it opens on the preset that names that window right now, or on
|
|
36
|
+
* custom with the pair in the fields.
|
|
25
37
|
*/
|
|
26
38
|
value?: DateRange;
|
|
27
39
|
/** The buttons offered. `id` may not be `"custom"`, which the picker reserves. */
|
|
28
40
|
presets?: readonly DateRangePreset[];
|
|
29
41
|
/**
|
|
30
|
-
* Which button starts selected — a preset id, or `"custom"`. Defaults to the
|
|
31
|
-
* to custom when
|
|
42
|
+
* Which button starts selected — a preset id, or `"custom"`. Defaults to the preset matching a
|
|
43
|
+
* seeded `value`, to custom when none does, and to the first preset when there is no `value`.
|
|
32
44
|
*/
|
|
33
45
|
preset?: string;
|
|
34
46
|
/**
|
|
@@ -56,6 +68,7 @@
|
|
|
56
68
|
|
|
57
69
|
let {
|
|
58
70
|
class: cls,
|
|
71
|
+
mode = "datetime",
|
|
59
72
|
value = $bindable(),
|
|
60
73
|
presets = DEFAULT_PRESETS,
|
|
61
74
|
preset,
|
|
@@ -76,7 +89,13 @@
|
|
|
76
89
|
// The selection is the picker's own state rather than a bound prop: the pair is the entire
|
|
77
90
|
// output, and a second bound value would be a second thing for a consumer to keep in step.
|
|
78
91
|
let selection = $state(
|
|
79
|
-
untrack(
|
|
92
|
+
untrack(
|
|
93
|
+
() =>
|
|
94
|
+
preset ??
|
|
95
|
+
(value
|
|
96
|
+
? selectionFor(value, presets, Date.now(), mode)
|
|
97
|
+
: (presets[0]?.id ?? CUSTOM_PRESET_ID)),
|
|
98
|
+
),
|
|
80
99
|
);
|
|
81
100
|
|
|
82
101
|
// Wall-clock text for the two fields. Empty until custom mode is entered, because reading the
|
|
@@ -86,6 +105,12 @@
|
|
|
86
105
|
let customTo = $state("");
|
|
87
106
|
|
|
88
107
|
const isCustom = $derived(selection === CUSTOM_PRESET_ID);
|
|
108
|
+
const days = $derived(mode === "date");
|
|
109
|
+
|
|
110
|
+
const options = $derived([
|
|
111
|
+
...presets.map(({ id, label }) => ({ id, label })),
|
|
112
|
+
{ id: CUSTOM_PRESET_ID, label: customLabel },
|
|
113
|
+
]);
|
|
89
114
|
|
|
90
115
|
const emit = (next: DateRange) => {
|
|
91
116
|
if (value?.from === next.from && value?.to === next.to) return;
|
|
@@ -95,17 +120,37 @@
|
|
|
95
120
|
};
|
|
96
121
|
|
|
97
122
|
const anchor = () => {
|
|
98
|
-
const next = anchorSelection(selection, presets, Date.now());
|
|
123
|
+
const next = anchorSelection(selection, presets, Date.now(), mode);
|
|
99
124
|
if (next) emit(next);
|
|
100
125
|
};
|
|
101
126
|
|
|
102
127
|
const seedCustomFields = (range: DateRange | undefined) => {
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
const toField = days ? parseDay : isoToLocalInput;
|
|
129
|
+
customFrom = toField(range?.from) ?? "";
|
|
130
|
+
customTo = toField(range?.to) ?? "";
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The typed pair, read back: `undefined` while either field is not yet a value — half typed —
|
|
135
|
+
* and otherwise the pair with whether it is in order. In `"date"` mode an empty field is a value,
|
|
136
|
+
* an open end, where in `"datetime"` mode it is not yet one.
|
|
137
|
+
*/
|
|
138
|
+
const typedPair = (): { range: DateRange; ordered: boolean } | undefined => {
|
|
139
|
+
if (days) {
|
|
140
|
+
const from = parseDay(customFrom);
|
|
141
|
+
const to = parseDay(customTo);
|
|
142
|
+
if (from === null || to === null) return undefined;
|
|
143
|
+
return { range: { from, to }, ordered: isValidDayRange(from, to) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const from = localInputToIso(customFrom);
|
|
147
|
+
const to = localInputToIso(customTo);
|
|
148
|
+
if (from === null || to === null) return undefined;
|
|
149
|
+
return { range: { from, to }, ordered: isValidRange(from, to) };
|
|
105
150
|
};
|
|
106
151
|
|
|
107
152
|
const enterCustom = () => {
|
|
108
|
-
const current = value ?? (presets[0] ? buildRange(presets[0]) : undefined);
|
|
153
|
+
const current = value ?? (presets[0] ? buildRange(presets[0], Date.now(), mode) : undefined);
|
|
109
154
|
|
|
110
155
|
seedCustomFields(current);
|
|
111
156
|
selection = CUSTOM_PRESET_ID;
|
|
@@ -117,27 +162,26 @@
|
|
|
117
162
|
};
|
|
118
163
|
|
|
119
164
|
const commitCustom = () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (from === null || to === null || !isValidRange(from, to)) return;
|
|
124
|
-
|
|
125
|
-
emit({ from, to });
|
|
165
|
+
const pair = typedPair();
|
|
166
|
+
if (pair?.ordered) emit(pair.range);
|
|
126
167
|
};
|
|
127
168
|
|
|
128
169
|
const rangeError = $derived.by(() => {
|
|
129
170
|
if (!isCustom) return null;
|
|
130
171
|
|
|
131
|
-
const from = localInputToIso(customFrom);
|
|
132
|
-
const to = localInputToIso(customTo);
|
|
133
|
-
|
|
134
172
|
// A half-typed field is not yet wrong, so it says nothing rather than flashing an error at
|
|
135
173
|
// someone who is still filling the form in.
|
|
136
|
-
|
|
174
|
+
const pair = typedPair();
|
|
175
|
+
if (pair === undefined) return null;
|
|
137
176
|
|
|
138
|
-
return
|
|
177
|
+
return pair.ordered ? null : invalidRangeLabel;
|
|
139
178
|
});
|
|
140
179
|
|
|
180
|
+
const pick = (id: string) => {
|
|
181
|
+
if (id === CUSTOM_PRESET_ID) enterCustom();
|
|
182
|
+
else selection = id;
|
|
183
|
+
};
|
|
184
|
+
|
|
141
185
|
$effect(() => {
|
|
142
186
|
const intervalMs = reAnchorMs;
|
|
143
187
|
if (selection === CUSTOM_PRESET_ID) return;
|
|
@@ -175,17 +219,21 @@
|
|
|
175
219
|
|
|
176
220
|
<!--
|
|
177
221
|
@component
|
|
178
|
-
A from/to picker built as a plain form control:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
A from/to picker built as a plain form control: presets, a custom pair the reader types, and
|
|
223
|
+
`{ from, to }` on the way out. It fetches nothing, knows no query parameter, and reads no URL — what
|
|
224
|
+
the pair means is the consumer's business.
|
|
225
|
+
|
|
226
|
+
It works in one of two modes. `"datetime"`, the default, is a window of instants: the custom
|
|
227
|
+
fields are `<input type="datetime-local">`, wall-clock time in the viewer's own zone, while the
|
|
228
|
+
emitted pair stays in UTC — the conversion runs to whole minutes in both directions; see
|
|
229
|
+
`isoToLocalInput` / `localInputToIso`. `"date"` is a span of calendar days: the fields are
|
|
230
|
+
`<input type="date">`, the pair is `YYYY-MM-DD`, and an empty end leaves that side open.
|
|
231
|
+
|
|
232
|
+
A preset is a duration back from now ("24h") or a window computed from the calendar ("this month",
|
|
233
|
+
built with `thisMonth`, `lastMonth`, `lastMonths` and `allTime`). Either re-anchors on a timer
|
|
234
|
+
(`reAnchorMs`, 60s by default, `0` to disable), so a dashboard left open keeps sliding forward and
|
|
235
|
+
"this month" turns into the next one. The timer suspends while the tab is hidden and fires once on
|
|
236
|
+
return. A custom range is never touched by it.
|
|
189
237
|
|
|
190
238
|
Every word it renders is a prop with an English default — `label`, `customLabel`, `fromLabel`,
|
|
191
239
|
`toLabel` and `invalidRangeLabel` — because the library ships no translations. Only the wording
|
|
@@ -194,33 +242,19 @@ buttons are labelled by the list itself, through `DateRangePreset.label`.
|
|
|
194
242
|
|
|
195
243
|
```svelte
|
|
196
244
|
<DateRangePicker bind:value={range} preset="24h" onchange={(r) => load(r)} />
|
|
245
|
+
|
|
246
|
+
<DateRangePicker
|
|
247
|
+
mode="date"
|
|
248
|
+
presets={[allTime("All time"), thisMonth("This month"), lastMonth("Last month")]}
|
|
249
|
+
onchange={(r) => load(r)}
|
|
250
|
+
/>
|
|
197
251
|
```
|
|
198
252
|
-->
|
|
199
253
|
|
|
200
254
|
<div class={["flex flex-wrap items-end gap-x-4 gap-y-3", cls]}>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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>
|
|
255
|
+
<!-- A segmented control rather than a joined row: a calendar picker easily offers six windows,
|
|
256
|
+
and a joined row cannot wrap onto a second line at phone width. -->
|
|
257
|
+
<SegmentedControl {options} value={selection} {label} onchange={pick} />
|
|
224
258
|
|
|
225
259
|
{#if isCustom}
|
|
226
260
|
<div class="flex flex-wrap items-end gap-x-4 gap-y-3">
|
|
@@ -230,8 +264,9 @@ buttons are labelled by the list itself, through `DateRangePreset.label`.
|
|
|
230
264
|
id={fromId}
|
|
231
265
|
class="input min-h-11"
|
|
232
266
|
class:input-error={rangeError !== null}
|
|
233
|
-
type="datetime-local"
|
|
267
|
+
type={days ? "date" : "datetime-local"}
|
|
234
268
|
value={customFrom}
|
|
269
|
+
max={days && customTo ? customTo : undefined}
|
|
235
270
|
aria-invalid={rangeError !== null}
|
|
236
271
|
aria-describedby={rangeError === null ? undefined : errorId}
|
|
237
272
|
oninput={({ currentTarget }) => {
|
|
@@ -247,8 +282,9 @@ buttons are labelled by the list itself, through `DateRangePreset.label`.
|
|
|
247
282
|
id={toId}
|
|
248
283
|
class="input min-h-11"
|
|
249
284
|
class:input-error={rangeError !== null}
|
|
250
|
-
type="datetime-local"
|
|
285
|
+
type={days ? "date" : "datetime-local"}
|
|
251
286
|
value={customTo}
|
|
287
|
+
min={days && customFrom ? customFrom : undefined}
|
|
252
288
|
aria-invalid={rangeError !== null}
|
|
253
289
|
aria-describedby={rangeError === null ? undefined : errorId}
|
|
254
290
|
oninput={({ currentTarget }) => {
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { ClassValue } from "svelte/elements";
|
|
2
|
-
import { type DateRange, type DateRangePreset } from "./date-range.js";
|
|
2
|
+
import { type DateRange, type DateRangeMode, type DateRangePreset } from "./date-range.js";
|
|
3
3
|
interface Props {
|
|
4
4
|
class?: ClassValue;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `"datetime"` (the default) picks a window of instants, emitted as UTC ISO strings, with
|
|
7
|
+
* date-and-time fields. `"date"` picks a span of calendar days, emitted as `YYYY-MM-DD`, with
|
|
8
|
+
* date fields either of which may stay empty to leave that side open.
|
|
9
|
+
*/
|
|
10
|
+
mode?: DateRangeMode;
|
|
11
|
+
/**
|
|
12
|
+
* The selected window — UTC ISO instants, or days in `"date"` mode. Bindable, and also reported
|
|
13
|
+
* through {@link Props.onchange} — a consumer needs one or the other, never both.
|
|
8
14
|
*
|
|
9
15
|
* It is an output, not an input: a relative preset overwrites it on every anchor. Passing one
|
|
10
|
-
* in only seeds the picker
|
|
16
|
+
* in only seeds the picker: it opens on the preset that names that window right now, or on
|
|
17
|
+
* custom with the pair in the fields.
|
|
11
18
|
*/
|
|
12
19
|
value?: DateRange;
|
|
13
20
|
/** The buttons offered. `id` may not be `"custom"`, which the picker reserves. */
|
|
14
21
|
presets?: readonly DateRangePreset[];
|
|
15
22
|
/**
|
|
16
|
-
* Which button starts selected — a preset id, or `"custom"`. Defaults to the
|
|
17
|
-
* to custom when
|
|
23
|
+
* Which button starts selected — a preset id, or `"custom"`. Defaults to the preset matching a
|
|
24
|
+
* seeded `value`, to custom when none does, and to the first preset when there is no `value`.
|
|
18
25
|
*/
|
|
19
26
|
preset?: string;
|
|
20
27
|
/**
|
|
@@ -40,17 +47,21 @@ interface Props {
|
|
|
40
47
|
onchange?: (value: DateRange) => void;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
|
-
* A from/to picker built as a plain form control:
|
|
44
|
-
*
|
|
45
|
-
*
|
|
50
|
+
* A from/to picker built as a plain form control: presets, a custom pair the reader types, and
|
|
51
|
+
* `{ from, to }` on the way out. It fetches nothing, knows no query parameter, and reads no URL — what
|
|
52
|
+
* the pair means is the consumer's business.
|
|
46
53
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
54
|
+
* It works in one of two modes. `"datetime"`, the default, is a window of instants: the custom
|
|
55
|
+
* fields are `<input type="datetime-local">`, wall-clock time in the viewer's own zone, while the
|
|
56
|
+
* emitted pair stays in UTC — the conversion runs to whole minutes in both directions; see
|
|
57
|
+
* `isoToLocalInput` / `localInputToIso`. `"date"` is a span of calendar days: the fields are
|
|
58
|
+
* `<input type="date">`, the pair is `YYYY-MM-DD`, and an empty end leaves that side open.
|
|
50
59
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
60
|
+
* A preset is a duration back from now ("24h") or a window computed from the calendar ("this month",
|
|
61
|
+
* built with `thisMonth`, `lastMonth`, `lastMonths` and `allTime`). Either re-anchors on a timer
|
|
62
|
+
* (`reAnchorMs`, 60s by default, `0` to disable), so a dashboard left open keeps sliding forward and
|
|
63
|
+
* "this month" turns into the next one. The timer suspends while the tab is hidden and fires once on
|
|
64
|
+
* return. A custom range is never touched by it.
|
|
54
65
|
*
|
|
55
66
|
* Every word it renders is a prop with an English default — `label`, `customLabel`, `fromLabel`,
|
|
56
67
|
* `toLabel` and `invalidRangeLabel` — because the library ships no translations. Only the wording
|
|
@@ -59,6 +70,12 @@ interface Props {
|
|
|
59
70
|
*
|
|
60
71
|
* ```svelte
|
|
61
72
|
* <DateRangePicker bind:value={range} preset="24h" onchange={(r) => load(r)} />
|
|
73
|
+
*
|
|
74
|
+
* <DateRangePicker
|
|
75
|
+
* mode="date"
|
|
76
|
+
* presets={[allTime("All time"), thisMonth("This month"), lastMonth("Last month")]}
|
|
77
|
+
* onchange={(r) => load(r)}
|
|
78
|
+
* />
|
|
62
79
|
* ```
|
|
63
80
|
*/
|
|
64
81
|
declare const DateRangePicker: import("svelte").Component<Props, {}, "value">;
|
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pair a range picker emits, and the whole of what it emits.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* produces. A string rather than a `Date`
|
|
6
|
-
* body, a query string, a store, a load
|
|
7
|
-
* exactly this string anyway, while an ISO
|
|
8
|
-
* sorts correctly with a plain `<`. UTC rather
|
|
9
|
-
* different zones must be able to name the same window
|
|
4
|
+
* In the default `"datetime"` mode both ends are ISO-8601 instants in UTC
|
|
5
|
+
* (`2026-08-29T18:04:05.123Z`), the form `toISOString()` produces. A string rather than a `Date`
|
|
6
|
+
* because this value's job is to travel: into a fetch body, a query string, a store, a load
|
|
7
|
+
* function's return — all of which serialise a `Date` to exactly this string anyway, while an ISO
|
|
8
|
+
* string additionally survives the round trip back and sorts correctly with a plain `<`. UTC rather
|
|
9
|
+
* than a local offset because two clients in different zones must be able to name the same window
|
|
10
|
+
* with the same characters.
|
|
11
|
+
*
|
|
12
|
+
* In `"date"` mode both ends are calendar days, `2026-08-29` — the value of an
|
|
13
|
+
* `<input type="date">`, with no time and no zone, because a day is what the reader picked. An
|
|
14
|
+
* empty string leaves that side open: `{ from: "2026-03-01", to: "" }` is "since March", and two
|
|
15
|
+
* empty ends are all time.
|
|
10
16
|
*/
|
|
11
17
|
export interface DateRange {
|
|
12
18
|
from: string;
|
|
13
19
|
to: string;
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* What the picker works in: `"datetime"` for a window of instants (a dashboard's last 24 hours),
|
|
23
|
+
* `"date"` for a span of calendar days (a history filtered to last month).
|
|
24
|
+
*/
|
|
25
|
+
export type DateRangeMode = "datetime" | "date";
|
|
26
|
+
interface PresetBase {
|
|
27
|
+
/** Stable across renders — it is what the picker's selection prop holds. */
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}
|
|
15
31
|
/**
|
|
16
32
|
* One relative window offered as a button — a fixed duration measured back from "now".
|
|
17
33
|
*
|
|
18
34
|
* `durationMs` is a duration and not a calendar step on purpose: "30d" means 30×24h, so the
|
|
19
35
|
* window keeps its length across a DST boundary instead of silently growing or shrinking an hour
|
|
20
|
-
* twice a year. A
|
|
36
|
+
* twice a year. A window that follows the calendar — this month, last month — is a
|
|
37
|
+
* {@link CalendarPreset}.
|
|
38
|
+
*
|
|
39
|
+
* In `"date"` mode the window becomes the days it touches: from the day `durationMs` ago to today.
|
|
21
40
|
*/
|
|
22
|
-
export interface
|
|
23
|
-
/** Stable across renders — it is what the picker's selection prop holds. */
|
|
24
|
-
id: string;
|
|
25
|
-
label: string;
|
|
41
|
+
export interface DurationPreset extends PresetBase {
|
|
26
42
|
durationMs: number;
|
|
43
|
+
range?: never;
|
|
27
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* One window offered as a button and computed from the current moment — for the windows a
|
|
47
|
+
* duration cannot name: this month, last month, the last six months, all time. `range` returns the
|
|
48
|
+
* pair in the picker's mode, and runs again on every re-anchor, so "this month" turns into the
|
|
49
|
+
* next one when the month does.
|
|
50
|
+
*
|
|
51
|
+
* {@link thisMonth}, {@link lastMonth}, {@link lastMonths} and {@link allTime} build the usual
|
|
52
|
+
* ones for `"date"` mode.
|
|
53
|
+
*/
|
|
54
|
+
export interface CalendarPreset extends PresetBase {
|
|
55
|
+
range: (now: Date) => DateRange;
|
|
56
|
+
durationMs?: never;
|
|
57
|
+
}
|
|
58
|
+
export type DateRangePreset = DurationPreset | CalendarPreset;
|
|
28
59
|
/**
|
|
29
60
|
* The selection value that means "the user typed the two ends themselves".
|
|
30
61
|
*
|
|
@@ -33,8 +64,10 @@ export interface DateRangePreset {
|
|
|
33
64
|
export declare const CUSTOM_PRESET_ID = "custom";
|
|
34
65
|
/** The four windows a dashboard actually gets asked for. Replace the list, not the component. */
|
|
35
66
|
export declare const DEFAULT_PRESETS: readonly DateRangePreset[];
|
|
36
|
-
/**
|
|
37
|
-
export declare const
|
|
67
|
+
/** A local calendar day as `YYYY-MM-DD`, the value an `<input type="date">` holds. */
|
|
68
|
+
export declare const toDay: (date: Date) => string;
|
|
69
|
+
/** Builds the window a preset names as of `now`, in the picker's `mode`. */
|
|
70
|
+
export declare const buildRange: (preset: DateRangePreset, now?: Date | number, mode?: DateRangeMode) => DateRange;
|
|
38
71
|
/**
|
|
39
72
|
* Resolves a selection into the window it stands for right now, or `null` when there is nothing
|
|
40
73
|
* to build — an unknown id, or {@link CUSTOM_PRESET_ID}.
|
|
@@ -43,7 +76,41 @@ export declare const buildRange: (preset: DateRangePreset, now?: Date | number)
|
|
|
43
76
|
* range is never re-anchored" a property of the data rather than of the timer's wiring: even a
|
|
44
77
|
* tick that somehow fired while the user was in custom mode would have nothing to emit.
|
|
45
78
|
*/
|
|
46
|
-
export declare const anchorSelection: (selection: string, presets: readonly DateRangePreset[], now?: Date | number) => DateRange | null;
|
|
79
|
+
export declare const anchorSelection: (selection: string, presets: readonly DateRangePreset[], now?: Date | number, mode?: DateRangeMode) => DateRange | null;
|
|
80
|
+
/**
|
|
81
|
+
* The selection a picker handed `value` (and no `preset`) should open on: the preset whose window
|
|
82
|
+
* right now is exactly that pair, or {@link CUSTOM_PRESET_ID}. A page that remounts its picker —
|
|
83
|
+
* one per tab, say — gets its "Last month" button back instead of a custom pair spelling it out.
|
|
84
|
+
*
|
|
85
|
+
* A duration preset in `"datetime"` mode only matches to the millisecond, so in practice a seeded
|
|
86
|
+
* instant pair still opens as custom, as it always did.
|
|
87
|
+
*/
|
|
88
|
+
export declare const selectionFor: (value: DateRange, presets: readonly DateRangePreset[], now?: Date | number, mode?: DateRangeMode) => string;
|
|
89
|
+
/**
|
|
90
|
+
* Whether a pair of days is in order. An empty end leaves that side open, so it can never put the
|
|
91
|
+
* pair out of order.
|
|
92
|
+
*/
|
|
93
|
+
export declare const isValidDayRange: (from: string, to: string) => boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Reads an `<input type="date">` value back: the day itself, `""` for an empty field — no bound on
|
|
96
|
+
* that side — or `null` for anything that is not yet a real day.
|
|
97
|
+
*
|
|
98
|
+
* A year below 1000 counts as not yet a day. Chrome reports a value on every digit of the year as
|
|
99
|
+
* it is typed — `0002-03-01`, `0020-03-01`, `0202-03-01` — and each of those would otherwise be
|
|
100
|
+
* emitted, and fetched, on the way to `2026`.
|
|
101
|
+
*/
|
|
102
|
+
export declare const parseDay: (value: string | null | undefined) => string | null;
|
|
103
|
+
/** "This month", for `"date"` mode: from the 1st, open at the end. */
|
|
104
|
+
export declare const thisMonth: (label: string, id?: string) => CalendarPreset;
|
|
105
|
+
/** "Last month", for `"date"` mode: its first day to its last. */
|
|
106
|
+
export declare const lastMonth: (label: string, id?: string) => CalendarPreset;
|
|
107
|
+
/**
|
|
108
|
+
* The last `months` months, for `"date"` mode: from the same day that many months ago — or the
|
|
109
|
+
* last day that month has — open at the end.
|
|
110
|
+
*/
|
|
111
|
+
export declare const lastMonths: (months: number, label: string, id?: string) => CalendarPreset;
|
|
112
|
+
/** "All time", for `"date"` mode: both ends open. */
|
|
113
|
+
export declare const allTime: (label: string, id?: string) => CalendarPreset;
|
|
47
114
|
/** Whether the pair is in order. An empty end is not a range, so it is not valid either. */
|
|
48
115
|
export declare const isValidRange: (from: string | null, to: string | null) => boolean;
|
|
49
116
|
/**
|
|
@@ -99,3 +166,4 @@ export declare const createReAnchor: (options: {
|
|
|
99
166
|
intervalMs: number;
|
|
100
167
|
onAnchor: () => void;
|
|
101
168
|
}) => ReAnchor;
|
|
169
|
+
export {};
|
|
@@ -14,9 +14,16 @@ export const DEFAULT_PRESETS = [
|
|
|
14
14
|
{ id: "7d", label: "7d", durationMs: 7 * DAY_MS },
|
|
15
15
|
{ id: "30d", label: "30d", durationMs: 30 * DAY_MS },
|
|
16
16
|
];
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const pad = (value, length = 2) => String(value).padStart(length, "0");
|
|
18
|
+
/** A local calendar day as `YYYY-MM-DD`, the value an `<input type="date">` holds. */
|
|
19
|
+
export const toDay = (date) => `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
20
|
+
/** Builds the window a preset names as of `now`, in the picker's `mode`. */
|
|
21
|
+
export const buildRange = (preset, now = Date.now(), mode = "datetime") => {
|
|
19
22
|
const end = now instanceof Date ? now.getTime() : now;
|
|
23
|
+
if (preset.range)
|
|
24
|
+
return preset.range(new Date(end));
|
|
25
|
+
if (mode === "date")
|
|
26
|
+
return { from: toDay(new Date(end - preset.durationMs)), to: toDay(new Date(end)) };
|
|
20
27
|
return { from: new Date(end - preset.durationMs).toISOString(), to: new Date(end).toISOString() };
|
|
21
28
|
};
|
|
22
29
|
/**
|
|
@@ -27,12 +34,87 @@ export const buildRange = (preset, now = Date.now()) => {
|
|
|
27
34
|
* range is never re-anchored" a property of the data rather than of the timer's wiring: even a
|
|
28
35
|
* tick that somehow fired while the user was in custom mode would have nothing to emit.
|
|
29
36
|
*/
|
|
30
|
-
export const anchorSelection = (selection, presets, now = Date.now()) => {
|
|
37
|
+
export const anchorSelection = (selection, presets, now = Date.now(), mode = "datetime") => {
|
|
31
38
|
if (selection === CUSTOM_PRESET_ID)
|
|
32
39
|
return null;
|
|
33
40
|
const preset = presets.find(({ id }) => id === selection);
|
|
34
|
-
return preset ? buildRange(preset, now) : null;
|
|
41
|
+
return preset ? buildRange(preset, now, mode) : null;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The selection a picker handed `value` (and no `preset`) should open on: the preset whose window
|
|
45
|
+
* right now is exactly that pair, or {@link CUSTOM_PRESET_ID}. A page that remounts its picker —
|
|
46
|
+
* one per tab, say — gets its "Last month" button back instead of a custom pair spelling it out.
|
|
47
|
+
*
|
|
48
|
+
* A duration preset in `"datetime"` mode only matches to the millisecond, so in practice a seeded
|
|
49
|
+
* instant pair still opens as custom, as it always did.
|
|
50
|
+
*/
|
|
51
|
+
export const selectionFor = (value, presets, now = Date.now(), mode = "datetime") => {
|
|
52
|
+
const match = presets.find((preset) => {
|
|
53
|
+
const range = buildRange(preset, now, mode);
|
|
54
|
+
return range.from === value.from && range.to === value.to;
|
|
55
|
+
});
|
|
56
|
+
return match?.id ?? CUSTOM_PRESET_ID;
|
|
35
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether a pair of days is in order. An empty end leaves that side open, so it can never put the
|
|
60
|
+
* pair out of order.
|
|
61
|
+
*/
|
|
62
|
+
export const isValidDayRange = (from, to) => !from || !to || from <= to;
|
|
63
|
+
const DAY_PATTERN = /^([1-9]\d{3})-(\d{2})-(\d{2})$/;
|
|
64
|
+
/**
|
|
65
|
+
* Reads an `<input type="date">` value back: the day itself, `""` for an empty field — no bound on
|
|
66
|
+
* that side — or `null` for anything that is not yet a real day.
|
|
67
|
+
*
|
|
68
|
+
* A year below 1000 counts as not yet a day. Chrome reports a value on every digit of the year as
|
|
69
|
+
* it is typed — `0002-03-01`, `0020-03-01`, `0202-03-01` — and each of those would otherwise be
|
|
70
|
+
* emitted, and fetched, on the way to `2026`.
|
|
71
|
+
*/
|
|
72
|
+
export const parseDay = (value) => {
|
|
73
|
+
if (!value)
|
|
74
|
+
return "";
|
|
75
|
+
const match = DAY_PATTERN.exec(value);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const [, year, month, day] = match;
|
|
79
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
80
|
+
return date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? value : null;
|
|
81
|
+
};
|
|
82
|
+
/** `now` moved back `months` calendar months, kept on its day or the last one the month has. */
|
|
83
|
+
const monthsBefore = (now, months) => {
|
|
84
|
+
const month = now.getMonth() - months;
|
|
85
|
+
const lastDay = new Date(now.getFullYear(), month + 1, 0).getDate();
|
|
86
|
+
return new Date(now.getFullYear(), month, Math.min(now.getDate(), lastDay));
|
|
87
|
+
};
|
|
88
|
+
/** "This month", for `"date"` mode: from the 1st, open at the end. */
|
|
89
|
+
export const thisMonth = (label, id = "this-month") => ({
|
|
90
|
+
id,
|
|
91
|
+
label,
|
|
92
|
+
range: (now) => ({ from: toDay(new Date(now.getFullYear(), now.getMonth(), 1)), to: "" }),
|
|
93
|
+
});
|
|
94
|
+
/** "Last month", for `"date"` mode: its first day to its last. */
|
|
95
|
+
export const lastMonth = (label, id = "last-month") => ({
|
|
96
|
+
id,
|
|
97
|
+
label,
|
|
98
|
+
range: (now) => ({
|
|
99
|
+
from: toDay(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
|
|
100
|
+
to: toDay(new Date(now.getFullYear(), now.getMonth(), 0)),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* The last `months` months, for `"date"` mode: from the same day that many months ago — or the
|
|
105
|
+
* last day that month has — open at the end.
|
|
106
|
+
*/
|
|
107
|
+
export const lastMonths = (months, label, id = `last-${months}-months`) => ({
|
|
108
|
+
id,
|
|
109
|
+
label,
|
|
110
|
+
range: (now) => ({ from: toDay(monthsBefore(now, months)), to: "" }),
|
|
111
|
+
});
|
|
112
|
+
/** "All time", for `"date"` mode: both ends open. */
|
|
113
|
+
export const allTime = (label, id = "all-time") => ({
|
|
114
|
+
id,
|
|
115
|
+
label,
|
|
116
|
+
range: () => ({ from: "", to: "" }),
|
|
117
|
+
});
|
|
36
118
|
/** Whether the pair is in order. An empty end is not a range, so it is not valid either. */
|
|
37
119
|
export const isValidRange = (from, to) => {
|
|
38
120
|
if (from === null || to === null)
|
|
@@ -41,7 +123,6 @@ export const isValidRange = (from, to) => {
|
|
|
41
123
|
const end = Date.parse(to);
|
|
42
124
|
return !Number.isNaN(start) && !Number.isNaN(end) && start <= end;
|
|
43
125
|
};
|
|
44
|
-
const pad = (value, length = 2) => String(value).padStart(length, "0");
|
|
45
126
|
const LOCAL_INPUT_PATTERN = /^(\d{4,6})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
46
127
|
/**
|
|
47
128
|
* Formats a UTC instant for the `value` of an `<input type="datetime-local">`, whose contents are
|
|
@@ -24,7 +24,7 @@ export { default as Skeleton } from "./Skeleton.svelte";
|
|
|
24
24
|
export { default as Tabs } from "./Tabs.svelte";
|
|
25
25
|
export { default as Tooltip } from "./Tooltip.svelte";
|
|
26
26
|
export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
|
|
27
|
-
export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localInputToIso, type DateRange, type DateRangePreset, } from "./date-range.js";
|
|
27
|
+
export { allTime, buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, lastMonth, lastMonths, localInputToIso, thisMonth, toDay, type CalendarPreset, type DateRange, type DateRangeMode, type DateRangePreset, type DurationPreset, } from "./date-range.js";
|
|
28
28
|
export { matchesSelectQuery, normalizeForSearch, type SelectOption } from "./select.js";
|
|
29
29
|
export { formatJsonText, parseJsonText, type JsonTextOptions, type JsonTextResult, } from "./json-text.js";
|
|
30
30
|
export { DEFAULT_JSON_VIEW_LABELS, type JsonViewLabels } from "./json-view.js";
|
package/dist/components/index.js
CHANGED
|
@@ -28,12 +28,12 @@ export { default as Tooltip } from "./Tooltip.svelte";
|
|
|
28
28
|
// copying the curve out of the library, which is how two versions of one shape start existing.
|
|
29
29
|
// Not for avatars: a profile picture cut to anything but a disc reads as a badge.
|
|
30
30
|
export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
|
|
31
|
-
// The half of `date-range.js` a consumer of the picker has a use for: the
|
|
32
|
-
// preset
|
|
33
|
-
// pairing their own field with a `DateRange` has to repeat otherwise. The rest -- the selection
|
|
31
|
+
// The half of `date-range.js` a consumer of the picker has a use for: the prop types, the preset
|
|
32
|
+
// list, the calendar preset builders and the reserved id they are written against, and the
|
|
33
|
+
// conversions anyone pairing their own field with a `DateRange` has to repeat otherwise. The rest -- the selection
|
|
34
34
|
// anchor, the pair validator and the re-anchor clock -- is how the picker drives itself, and the
|
|
35
35
|
// picker never emits a pair that would fail the validator.
|
|
36
|
-
export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localInputToIso, } from "./date-range.js";
|
|
36
|
+
export { allTime, buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, lastMonth, lastMonths, localInputToIso, thisMonth, toDay, } from "./date-range.js";
|
|
37
37
|
// The half of `select.ts` a consumer has a use for: the option type every caller of either
|
|
38
38
|
// control has to name, and the default matcher, so widening the search -- matching the value too,
|
|
39
39
|
// a synonym list -- can compose with the accent folding instead of reimplementing it. The
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export declare const LIBRARY_VERSION = "0.
|
|
8
|
+
export declare const LIBRARY_VERSION = "0.6.0";
|
|
9
9
|
export { AsyncAction, type AsyncActionStatus, type AsyncFn } from "./async-action.svelte.js";
|
|
10
10
|
export { reportError, setErrorReporter, type ErrorReporter } from "./error.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export const LIBRARY_VERSION = "0.
|
|
8
|
+
export const LIBRARY_VERSION = "0.6.0";
|
|
9
9
|
// The root entry carries only what is not tied to one area: the error channel an app installs
|
|
10
10
|
// once, and the async state machine the components are built on. Everything visual is reached
|
|
11
11
|
// through a subpath — `/theme`, `/attachments`, `/components`, `/toast` — so importing a button
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viniaraujo68/plinth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A personal Svelte 5 design system: theme, application shell, interface primitives, routing metadata, a fetch wrapper and locale-aware formatters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|