@lyeve-labs/ui-kit 0.11.2 → 0.13.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 +1 -1
- package/dist/components/AccordionItem.svelte +1 -1
- package/dist/components/Autocomplete.svelte +191 -125
- package/dist/components/Autocomplete.svelte.d.ts +29 -8
- package/dist/components/Button.svelte +26 -4
- package/dist/components/Card.svelte +61 -3
- package/dist/components/Card.svelte.d.ts +24 -2
- package/dist/components/Checkbox.svelte +174 -59
- package/dist/components/Checkbox.svelte.d.ts +20 -3
- package/dist/components/CheckboxGroup.svelte +162 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
- package/dist/components/Collapsible.svelte +142 -0
- package/dist/components/Collapsible.svelte.d.ts +32 -0
- package/dist/components/CopyButton.svelte +126 -0
- package/dist/components/CopyButton.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +48 -6
- package/dist/components/DateTimePicker.svelte +337 -0
- package/dist/components/DateTimePicker.svelte.d.ts +26 -0
- package/dist/components/DescriptionList.svelte +78 -0
- package/dist/components/DescriptionList.svelte.d.ts +34 -0
- package/dist/components/Drawer.svelte +15 -4
- package/dist/components/Field.svelte +104 -0
- package/dist/components/Field.svelte.d.ts +46 -0
- package/dist/components/FileInput.svelte +5 -2
- package/dist/components/FormMessage.svelte +85 -0
- package/dist/components/FormMessage.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +1 -1
- package/dist/components/Label.svelte +7 -1
- package/dist/components/Label.svelte.d.ts +6 -0
- package/dist/components/Modal.svelte +25 -8
- package/dist/components/MultiSelect.svelte +199 -109
- package/dist/components/MultiSelect.svelte.d.ts +22 -9
- package/dist/components/NumberInput.svelte +8 -4
- package/dist/components/PageHeader.svelte +37 -4
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/PageShell.svelte +85 -0
- package/dist/components/PageShell.svelte.d.ts +38 -0
- package/dist/components/Pagination.svelte +58 -17
- package/dist/components/Panel.svelte +101 -0
- package/dist/components/Panel.svelte.d.ts +39 -0
- package/dist/components/PasswordInput.svelte +139 -0
- package/dist/components/PasswordInput.svelte.d.ts +29 -0
- package/dist/components/Radio.svelte +152 -32
- package/dist/components/Radio.svelte.d.ts +16 -1
- package/dist/components/RadioGroup.svelte +118 -71
- package/dist/components/RadioGroup.svelte.d.ts +39 -9
- package/dist/components/SectionHeading.svelte +39 -0
- package/dist/components/SectionHeading.svelte.d.ts +21 -0
- package/dist/components/SegmentedControl.svelte +194 -0
- package/dist/components/SegmentedControl.svelte.d.ts +55 -0
- package/dist/components/Select.svelte +471 -46
- package/dist/components/Select.svelte.d.ts +95 -6
- package/dist/components/SidebarNav.svelte +259 -0
- package/dist/components/SidebarNav.svelte.d.ts +17 -0
- package/dist/components/Stat.svelte +53 -2
- package/dist/components/Stat.svelte.d.ts +31 -0
- package/dist/components/Textarea.svelte +1 -1
- package/dist/components/TimePicker.svelte +480 -0
- package/dist/components/TimePicker.svelte.d.ts +23 -0
- package/dist/components/Toaster.svelte +9 -2
- package/dist/components/Toggle.svelte +5 -1
- package/dist/components/Toggle.svelte.d.ts +2 -0
- package/dist/components/Toolbar.svelte +39 -0
- package/dist/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/Tooltip.svelte +48 -12
- package/dist/components/TreeView.svelte +339 -0
- package/dist/components/TreeView.svelte.d.ts +37 -0
- package/dist/components/dialog/Dialog.svelte +15 -58
- package/dist/components/dialog/dialog-manager.svelte.d.ts +2 -2
- package/dist/components/dialog/dialog-manager.svelte.js +21 -21
- package/dist/index.d.ts +25 -1
- package/dist/index.js +20 -1
- package/dist/internal/calendar.d.ts +119 -0
- package/dist/internal/calendar.js +225 -0
- package/dist/internal/choice.d.ts +136 -0
- package/dist/internal/choice.js +179 -0
- package/dist/internal/field.d.ts +31 -0
- package/dist/internal/field.js +42 -1
- package/dist/internal/filter.d.ts +80 -0
- package/dist/internal/filter.js +80 -0
- package/dist/internal/layout.d.ts +119 -0
- package/dist/internal/layout.js +132 -0
- package/dist/internal/listbox.svelte.d.ts +77 -0
- package/dist/internal/listbox.svelte.js +438 -0
- package/dist/internal/nav-expansion.svelte.d.ts +36 -0
- package/dist/internal/nav-expansion.svelte.js +144 -0
- package/dist/internal/nav-tree.d.ts +68 -0
- package/dist/internal/nav-tree.js +102 -0
- package/dist/internal/overlay.d.ts +25 -0
- package/dist/internal/overlay.js +92 -0
- package/dist/internal/panel.d.ts +100 -0
- package/dist/internal/panel.js +109 -0
- package/dist/internal/rollup.d.ts +52 -0
- package/dist/internal/rollup.js +67 -0
- package/dist/internal/time.d.ts +103 -0
- package/dist/internal/time.js +166 -0
- package/dist/internal/tree.d.ts +86 -0
- package/dist/internal/tree.js +111 -0
- package/dist/styles/theme.css +66 -25
- package/package.json +4 -2
- package/src/lib/styles/theme.css +66 -25
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tri-state contract behind a permissions matrix and a checkable tree.
|
|
3
|
+
*
|
|
4
|
+
* A parent control does two jobs at once: it summarises the rows beneath it,
|
|
5
|
+
* and a click on it writes every one of them. Both surfaces folded that summary
|
|
6
|
+
* with `rows.every(has)`, which is true over an empty array, so a group whose
|
|
7
|
+
* rows were all filtered away or all disabled drew as fully granted and the
|
|
8
|
+
* click that followed meant clear rather than fill. Summarising by count rather
|
|
9
|
+
* than by fold, and separating what may be written from what is counted, is the
|
|
10
|
+
* whole reason this module exists.
|
|
11
|
+
*
|
|
12
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
13
|
+
*/
|
|
14
|
+
/** What a parent control shows: nothing granted, part granted, all granted. */
|
|
15
|
+
export type TriState = 'none' | 'some' | 'all';
|
|
16
|
+
/**
|
|
17
|
+
* Rolls a set of rows up to one state.
|
|
18
|
+
*
|
|
19
|
+
* The empty set rolls up to 'none'. Array.prototype.every returns true on an
|
|
20
|
+
* empty array, so the obvious implementation reports a fully granted column
|
|
21
|
+
* over zero rows, and the next click on that column is a bulk write the
|
|
22
|
+
* operator never asked for.
|
|
23
|
+
*/
|
|
24
|
+
export declare function rollUp<T>(items: readonly T[], has: (item: T) => boolean): TriState;
|
|
25
|
+
/**
|
|
26
|
+
* What a click on a rolled-up control means: 'all' clears, anything else fills.
|
|
27
|
+
*
|
|
28
|
+
* A 'some' state must fill rather than clear, because the partial state most
|
|
29
|
+
* often means the operator is part way through granting.
|
|
30
|
+
*/
|
|
31
|
+
export declare function nextState(current: TriState): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Applies a value across a subtree, skipping items that cannot take it.
|
|
34
|
+
*
|
|
35
|
+
* The exclusion is the case that is always got wrong: a disabled row must
|
|
36
|
+
* neither be written nor counted in the roll-up that follows. Returning the
|
|
37
|
+
* writes rather than performing them is what lets the caller feed the same list
|
|
38
|
+
* to both, so the summary can never describe a row the write skipped.
|
|
39
|
+
*/
|
|
40
|
+
export declare function setSubtree<T>(items: readonly T[], value: boolean, canSet: (item: T) => boolean): {
|
|
41
|
+
item: T;
|
|
42
|
+
value: boolean;
|
|
43
|
+
}[];
|
|
44
|
+
/**
|
|
45
|
+
* Rolls up while treating a floor as already granted.
|
|
46
|
+
*
|
|
47
|
+
* A grant that is inherited rather than stored cannot be revoked by clearing
|
|
48
|
+
* the stored row, so a control that offers to clear it is lying. A row that is
|
|
49
|
+
* floored counts as granted here, which puts the parent at 'all' and makes the
|
|
50
|
+
* next click fill rather than clear.
|
|
51
|
+
*/
|
|
52
|
+
export declare function rollUpWithFloor<T>(items: readonly T[], has: (item: T) => boolean, floor: (item: T) => boolean): TriState;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tri-state contract behind a permissions matrix and a checkable tree.
|
|
3
|
+
*
|
|
4
|
+
* A parent control does two jobs at once: it summarises the rows beneath it,
|
|
5
|
+
* and a click on it writes every one of them. Both surfaces folded that summary
|
|
6
|
+
* with `rows.every(has)`, which is true over an empty array, so a group whose
|
|
7
|
+
* rows were all filtered away or all disabled drew as fully granted and the
|
|
8
|
+
* click that followed meant clear rather than fill. Summarising by count rather
|
|
9
|
+
* than by fold, and separating what may be written from what is counted, is the
|
|
10
|
+
* whole reason this module exists.
|
|
11
|
+
*
|
|
12
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Rolls a set of rows up to one state.
|
|
16
|
+
*
|
|
17
|
+
* The empty set rolls up to 'none'. Array.prototype.every returns true on an
|
|
18
|
+
* empty array, so the obvious implementation reports a fully granted column
|
|
19
|
+
* over zero rows, and the next click on that column is a bulk write the
|
|
20
|
+
* operator never asked for.
|
|
21
|
+
*/
|
|
22
|
+
export function rollUp(items, has) {
|
|
23
|
+
let granted = 0;
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (has(item))
|
|
26
|
+
granted++;
|
|
27
|
+
}
|
|
28
|
+
if (granted === 0)
|
|
29
|
+
return 'none';
|
|
30
|
+
return granted === items.length ? 'all' : 'some';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What a click on a rolled-up control means: 'all' clears, anything else fills.
|
|
34
|
+
*
|
|
35
|
+
* A 'some' state must fill rather than clear, because the partial state most
|
|
36
|
+
* often means the operator is part way through granting.
|
|
37
|
+
*/
|
|
38
|
+
export function nextState(current) {
|
|
39
|
+
return current !== 'all';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Applies a value across a subtree, skipping items that cannot take it.
|
|
43
|
+
*
|
|
44
|
+
* The exclusion is the case that is always got wrong: a disabled row must
|
|
45
|
+
* neither be written nor counted in the roll-up that follows. Returning the
|
|
46
|
+
* writes rather than performing them is what lets the caller feed the same list
|
|
47
|
+
* to both, so the summary can never describe a row the write skipped.
|
|
48
|
+
*/
|
|
49
|
+
export function setSubtree(items, value, canSet) {
|
|
50
|
+
const writes = [];
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (canSet(item))
|
|
53
|
+
writes.push({ item, value });
|
|
54
|
+
}
|
|
55
|
+
return writes;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rolls up while treating a floor as already granted.
|
|
59
|
+
*
|
|
60
|
+
* A grant that is inherited rather than stored cannot be revoked by clearing
|
|
61
|
+
* the stored row, so a control that offers to clear it is lying. A row that is
|
|
62
|
+
* floored counts as granted here, which puts the parent at 'all' and makes the
|
|
63
|
+
* next click fill rather than clear.
|
|
64
|
+
*/
|
|
65
|
+
export function rollUpWithFloor(items, has, floor) {
|
|
66
|
+
return rollUp(items, (item) => floor(item) || has(item));
|
|
67
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic behind TimePicker's hour, minute and optional second segments.
|
|
3
|
+
*
|
|
4
|
+
* The value the field reads and writes is RFC 3339 partial-time with no
|
|
5
|
+
* fraction:
|
|
6
|
+
*
|
|
7
|
+
* unset ''
|
|
8
|
+
* seconds: false /^([01]\d|2[0-3]):([0-5]\d)$/ for example '09:30'
|
|
9
|
+
* seconds: true /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/ for example '09:30:15'
|
|
10
|
+
*
|
|
11
|
+
* 24-hour, zero padded, no offset, no fractional seconds, no '24:00', no ':60'.
|
|
12
|
+
*
|
|
13
|
+
* Every helper here is a step a component author gets wrong on the first
|
|
14
|
+
* attempt: hours that carry when the arrow key promised one segment, a range
|
|
15
|
+
* check that cannot express an overnight window, per-segment clamping that
|
|
16
|
+
* rewrites a legal time, and a modulo that turns midnight into hour zero of a
|
|
17
|
+
* clock with no hour zero.
|
|
18
|
+
*
|
|
19
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
20
|
+
*/
|
|
21
|
+
/** One time, already range checked. `mi` rather than `m` so it cannot read as months. */
|
|
22
|
+
export interface TimeParts {
|
|
23
|
+
h: number;
|
|
24
|
+
mi: number;
|
|
25
|
+
s: number;
|
|
26
|
+
}
|
|
27
|
+
/** The parts of the field an arrow key can land on. `meridiem` exists only in 12-hour display. */
|
|
28
|
+
export type TimeSegment = 'hour' | 'minute' | 'second' | 'meridiem';
|
|
29
|
+
/**
|
|
30
|
+
* Range-checks rather than wrapping: '24:00' and '00:60' return null, not a
|
|
31
|
+
* rolled-over value.
|
|
32
|
+
*
|
|
33
|
+
* Parsing through Date was the first attempt and it accepts both, because
|
|
34
|
+
* '1970-01-01T24:00' is the following midnight. A picker fed that displayed
|
|
35
|
+
* 00:00 for a string the server had already rejected. A single unpadded digit
|
|
36
|
+
* such as '9:30' is rejected for the same reason: the segments are fixed width,
|
|
37
|
+
* and accepting a short one lets a keystroke part way through an entry read as
|
|
38
|
+
* a committed value.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseISOTime(s: string | undefined | null): TimeParts | null;
|
|
41
|
+
/**
|
|
42
|
+
* Writes a time back in the one format the field accepts.
|
|
43
|
+
*
|
|
44
|
+
* `seconds` decides whether the third segment is written at all. A picker with
|
|
45
|
+
* no second segment that emits '09:30:00' round-trips a field the user cannot
|
|
46
|
+
* see and can never correct.
|
|
47
|
+
*/
|
|
48
|
+
export declare function toISOTime(p: TimeParts, seconds: boolean): string;
|
|
49
|
+
/**
|
|
50
|
+
* Steps one segment without carrying into its neighbour. Stepping the minute
|
|
51
|
+
* past 59 wraps to 0 and leaves the hour alone, because a spinner that changes
|
|
52
|
+
* two fields at once is not what the arrow key promised.
|
|
53
|
+
*
|
|
54
|
+
* A step that does not divide its segment lands on the far end rather than
|
|
55
|
+
* carrying the remainder: the minute 59 with step 15 goes to 0, not to 14, and
|
|
56
|
+
* stepping down from 0 goes to 59, not to 45. The remainder is a value the user
|
|
57
|
+
* cannot predict from the key they pressed.
|
|
58
|
+
*
|
|
59
|
+
* `meridiem` has two values, so it ignores `step` and moves the hour by twelve
|
|
60
|
+
* once per unit of delta. An even delta lands back where it started.
|
|
61
|
+
*/
|
|
62
|
+
export declare function stepSegment(p: TimeParts, segment: TimeSegment, delta: number, step: number): TimeParts;
|
|
63
|
+
/**
|
|
64
|
+
* Inclusive bounds. A range whose min is greater than its max wraps past
|
|
65
|
+
* midnight and is a legal way to say "overnight", so the check is a disjunction
|
|
66
|
+
* rather than a conjunction in that case.
|
|
67
|
+
*
|
|
68
|
+
* The conjunction alone makes 22:00 to 06:00 match nothing, and a night shift
|
|
69
|
+
* picker built on it rejected every time a user could enter.
|
|
70
|
+
*/
|
|
71
|
+
export declare function withinTimeRange(p: TimeParts, min: TimeParts | null, max: TimeParts | null): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Clamps a whole time to the bounds. Clamping per segment is wrong: with min
|
|
74
|
+
* 09:30, the time 10:15 is legal and per-segment clamping would push its minute
|
|
75
|
+
* to 30.
|
|
76
|
+
*
|
|
77
|
+
* When the range wraps past midnight the excluded window is the gap between max
|
|
78
|
+
* and min, so a time inside it moves to whichever end is nearer around the
|
|
79
|
+
* clock. A tie moves back to max, because the user was on the earlier side of
|
|
80
|
+
* the window before the step that left it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function clampTime(p: TimeParts, min: TimeParts | null, max: TimeParts | null): TimeParts;
|
|
83
|
+
/**
|
|
84
|
+
* 24-hour to 12-hour display, returning the hour and the meridiem. Midnight is
|
|
85
|
+
* 12 AM and noon is 12 PM, which is where the naive modulo gets it wrong: it
|
|
86
|
+
* gives 0 for both, and a picker showing '0:00 AM' has invented an hour zero
|
|
87
|
+
* that no 12-hour clock has.
|
|
88
|
+
*/
|
|
89
|
+
export declare function to12Hour(h: number): {
|
|
90
|
+
hour: number;
|
|
91
|
+
meridiem: 'AM' | 'PM';
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* 12-hour back to 24. 12 AM is hour 0 and 12 PM is hour 12; every other hour is
|
|
95
|
+
* itself in the morning and itself plus twelve in the afternoon.
|
|
96
|
+
*/
|
|
97
|
+
export declare function from12Hour(hour: number, meridiem: 'AM' | 'PM'): number;
|
|
98
|
+
/**
|
|
99
|
+
* Zero-pads a segment for display. A minute of 5 written straight into the
|
|
100
|
+
* value gives '09:5', which parseISOTime then rejects, so the field drops the
|
|
101
|
+
* time the user just picked and blanks itself.
|
|
102
|
+
*/
|
|
103
|
+
export declare function pad2(n: number): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic behind TimePicker's hour, minute and optional second segments.
|
|
3
|
+
*
|
|
4
|
+
* The value the field reads and writes is RFC 3339 partial-time with no
|
|
5
|
+
* fraction:
|
|
6
|
+
*
|
|
7
|
+
* unset ''
|
|
8
|
+
* seconds: false /^([01]\d|2[0-3]):([0-5]\d)$/ for example '09:30'
|
|
9
|
+
* seconds: true /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/ for example '09:30:15'
|
|
10
|
+
*
|
|
11
|
+
* 24-hour, zero padded, no offset, no fractional seconds, no '24:00', no ':60'.
|
|
12
|
+
*
|
|
13
|
+
* Every helper here is a step a component author gets wrong on the first
|
|
14
|
+
* attempt: hours that carry when the arrow key promised one segment, a range
|
|
15
|
+
* check that cannot express an overnight window, per-segment clamping that
|
|
16
|
+
* rewrites a legal time, and a modulo that turns midnight into hour zero of a
|
|
17
|
+
* clock with no hour zero.
|
|
18
|
+
*
|
|
19
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
20
|
+
*/
|
|
21
|
+
/** Highest legal value plus one, per stepped segment. */
|
|
22
|
+
const SEGMENT_SIZE = {
|
|
23
|
+
hour: 24,
|
|
24
|
+
minute: 60,
|
|
25
|
+
second: 60,
|
|
26
|
+
};
|
|
27
|
+
const SECONDS_PER_MINUTE = 60;
|
|
28
|
+
const SECONDS_PER_HOUR = 3600;
|
|
29
|
+
/** The whole time as one number, so a comparison cannot compare the minute first. */
|
|
30
|
+
function secondsOf(p) {
|
|
31
|
+
return p.h * SECONDS_PER_HOUR + p.mi * SECONDS_PER_MINUTE + p.s;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The segments are fixed width and the third is optional, which is the whole
|
|
35
|
+
* grammar. Anchored at both ends so a trailing offset or a fraction fails.
|
|
36
|
+
*/
|
|
37
|
+
const ISO_TIME = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/;
|
|
38
|
+
/**
|
|
39
|
+
* Range-checks rather than wrapping: '24:00' and '00:60' return null, not a
|
|
40
|
+
* rolled-over value.
|
|
41
|
+
*
|
|
42
|
+
* Parsing through Date was the first attempt and it accepts both, because
|
|
43
|
+
* '1970-01-01T24:00' is the following midnight. A picker fed that displayed
|
|
44
|
+
* 00:00 for a string the server had already rejected. A single unpadded digit
|
|
45
|
+
* such as '9:30' is rejected for the same reason: the segments are fixed width,
|
|
46
|
+
* and accepting a short one lets a keystroke part way through an entry read as
|
|
47
|
+
* a committed value.
|
|
48
|
+
*/
|
|
49
|
+
export function parseISOTime(s) {
|
|
50
|
+
if (typeof s !== 'string')
|
|
51
|
+
return null;
|
|
52
|
+
const m = ISO_TIME.exec(s);
|
|
53
|
+
if (!m)
|
|
54
|
+
return null;
|
|
55
|
+
const sec = m[3];
|
|
56
|
+
return { h: Number(m[1]), mi: Number(m[2]), s: sec ? Number(sec) : 0 };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Writes a time back in the one format the field accepts.
|
|
60
|
+
*
|
|
61
|
+
* `seconds` decides whether the third segment is written at all. A picker with
|
|
62
|
+
* no second segment that emits '09:30:00' round-trips a field the user cannot
|
|
63
|
+
* see and can never correct.
|
|
64
|
+
*/
|
|
65
|
+
export function toISOTime(p, seconds) {
|
|
66
|
+
const base = `${pad2(p.h)}:${pad2(p.mi)}`;
|
|
67
|
+
return seconds ? `${base}:${pad2(p.s)}` : base;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Steps one segment without carrying into its neighbour. Stepping the minute
|
|
71
|
+
* past 59 wraps to 0 and leaves the hour alone, because a spinner that changes
|
|
72
|
+
* two fields at once is not what the arrow key promised.
|
|
73
|
+
*
|
|
74
|
+
* A step that does not divide its segment lands on the far end rather than
|
|
75
|
+
* carrying the remainder: the minute 59 with step 15 goes to 0, not to 14, and
|
|
76
|
+
* stepping down from 0 goes to 59, not to 45. The remainder is a value the user
|
|
77
|
+
* cannot predict from the key they pressed.
|
|
78
|
+
*
|
|
79
|
+
* `meridiem` has two values, so it ignores `step` and moves the hour by twelve
|
|
80
|
+
* once per unit of delta. An even delta lands back where it started.
|
|
81
|
+
*/
|
|
82
|
+
export function stepSegment(p, segment, delta, step) {
|
|
83
|
+
if (segment === 'meridiem') {
|
|
84
|
+
const flips = Math.abs(Math.trunc(delta)) % 2;
|
|
85
|
+
return { ...p, h: (p.h + 12 * flips) % SEGMENT_SIZE.hour };
|
|
86
|
+
}
|
|
87
|
+
const size = SEGMENT_SIZE[segment];
|
|
88
|
+
const current = segment === 'hour' ? p.h : segment === 'minute' ? p.mi : p.s;
|
|
89
|
+
const next = current + delta * step;
|
|
90
|
+
const moved = next > size - 1 ? 0 : next < 0 ? size - 1 : next;
|
|
91
|
+
if (segment === 'hour')
|
|
92
|
+
return { ...p, h: moved };
|
|
93
|
+
if (segment === 'minute')
|
|
94
|
+
return { ...p, mi: moved };
|
|
95
|
+
return { ...p, s: moved };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Inclusive bounds. A range whose min is greater than its max wraps past
|
|
99
|
+
* midnight and is a legal way to say "overnight", so the check is a disjunction
|
|
100
|
+
* rather than a conjunction in that case.
|
|
101
|
+
*
|
|
102
|
+
* The conjunction alone makes 22:00 to 06:00 match nothing, and a night shift
|
|
103
|
+
* picker built on it rejected every time a user could enter.
|
|
104
|
+
*/
|
|
105
|
+
export function withinTimeRange(p, min, max) {
|
|
106
|
+
const t = secondsOf(p);
|
|
107
|
+
if (min !== null && max !== null) {
|
|
108
|
+
const lo = secondsOf(min);
|
|
109
|
+
const hi = secondsOf(max);
|
|
110
|
+
return lo > hi ? t >= lo || t <= hi : t >= lo && t <= hi;
|
|
111
|
+
}
|
|
112
|
+
if (min !== null)
|
|
113
|
+
return t >= secondsOf(min);
|
|
114
|
+
if (max !== null)
|
|
115
|
+
return t <= secondsOf(max);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Clamps a whole time to the bounds. Clamping per segment is wrong: with min
|
|
120
|
+
* 09:30, the time 10:15 is legal and per-segment clamping would push its minute
|
|
121
|
+
* to 30.
|
|
122
|
+
*
|
|
123
|
+
* When the range wraps past midnight the excluded window is the gap between max
|
|
124
|
+
* and min, so a time inside it moves to whichever end is nearer around the
|
|
125
|
+
* clock. A tie moves back to max, because the user was on the earlier side of
|
|
126
|
+
* the window before the step that left it.
|
|
127
|
+
*/
|
|
128
|
+
export function clampTime(p, min, max) {
|
|
129
|
+
if (withinTimeRange(p, min, max))
|
|
130
|
+
return p;
|
|
131
|
+
if (min !== null && max !== null && secondsOf(min) > secondsOf(max)) {
|
|
132
|
+
const t = secondsOf(p);
|
|
133
|
+
return t - secondsOf(max) <= secondsOf(min) - t ? max : min;
|
|
134
|
+
}
|
|
135
|
+
if (min !== null && secondsOf(p) < secondsOf(min))
|
|
136
|
+
return min;
|
|
137
|
+
if (max !== null && secondsOf(p) > secondsOf(max))
|
|
138
|
+
return max;
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 24-hour to 12-hour display, returning the hour and the meridiem. Midnight is
|
|
143
|
+
* 12 AM and noon is 12 PM, which is where the naive modulo gets it wrong: it
|
|
144
|
+
* gives 0 for both, and a picker showing '0:00 AM' has invented an hour zero
|
|
145
|
+
* that no 12-hour clock has.
|
|
146
|
+
*/
|
|
147
|
+
export function to12Hour(h) {
|
|
148
|
+
const wrapped = h % 12;
|
|
149
|
+
return { hour: wrapped === 0 ? 12 : wrapped, meridiem: h < 12 ? 'AM' : 'PM' };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 12-hour back to 24. 12 AM is hour 0 and 12 PM is hour 12; every other hour is
|
|
153
|
+
* itself in the morning and itself plus twelve in the afternoon.
|
|
154
|
+
*/
|
|
155
|
+
export function from12Hour(hour, meridiem) {
|
|
156
|
+
const base = hour % 12;
|
|
157
|
+
return meridiem === 'PM' ? base + 12 : base;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Zero-pads a segment for display. A minute of 5 written straight into the
|
|
161
|
+
* value gives '09:5', which parseISOTime then rejects, so the field drops the
|
|
162
|
+
* time the user just picked and blanks itself.
|
|
163
|
+
*/
|
|
164
|
+
export function pad2(n) {
|
|
165
|
+
return String(n).padStart(2, '0');
|
|
166
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a data tree and the arithmetic a checkable one needs.
|
|
3
|
+
*
|
|
4
|
+
* A data tree is not a navigation menu. A menu shows where you can go and one
|
|
5
|
+
* item is current; a tree shows a structure, and a branch in it summarises
|
|
6
|
+
* everything beneath it while a click on that branch writes every one of them.
|
|
7
|
+
* Those two jobs are the reason this module exists: the summary and the write
|
|
8
|
+
* have to be computed from the same list, or a branch reports a state its own
|
|
9
|
+
* click does not produce.
|
|
10
|
+
*
|
|
11
|
+
* The tri-state itself comes from rollup.ts rather than being written again
|
|
12
|
+
* here. That module already holds the case a fold gets wrong, and a second
|
|
13
|
+
* implementation of it would be a second chance to get it wrong.
|
|
14
|
+
*
|
|
15
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
16
|
+
*/
|
|
17
|
+
import type { Component } from 'svelte';
|
|
18
|
+
import { type TriState } from './rollup.js';
|
|
19
|
+
/** Re-exported so a consumer of a branch's state does not have to know where the arithmetic lives. */
|
|
20
|
+
export type { TriState };
|
|
21
|
+
/** One node of a data tree. */
|
|
22
|
+
export interface TreeNode<T = unknown> {
|
|
23
|
+
/** Stable across renders. */
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
icon?: Component<{
|
|
27
|
+
size?: number;
|
|
28
|
+
class?: string;
|
|
29
|
+
}>;
|
|
30
|
+
children?: TreeNode<T>[];
|
|
31
|
+
/** A count or status beside the label. */
|
|
32
|
+
badge?: string | number;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** Whatever the consumer needs on activation. */
|
|
35
|
+
data?: T;
|
|
36
|
+
}
|
|
37
|
+
/** One visible row, in the order a keyboard walks them. */
|
|
38
|
+
export interface TreeRow<T = unknown> {
|
|
39
|
+
node: TreeNode<T>;
|
|
40
|
+
depth: number;
|
|
41
|
+
parentId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a node has anything to descend into.
|
|
45
|
+
*
|
|
46
|
+
* An empty `children` array is a leaf, not a branch that happens to be empty.
|
|
47
|
+
* A node that announces `aria-expanded` and then opens onto nothing tells a
|
|
48
|
+
* screen reader user there is more to read when there is not.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isBranch<T>(node: TreeNode<T>): boolean;
|
|
51
|
+
/** Every leaf under a node, excluding disabled ones, because a disabled leaf must be neither written nor counted. */
|
|
52
|
+
export declare function leavesOf<T>(node: TreeNode<T>): TreeNode<T>[];
|
|
53
|
+
/**
|
|
54
|
+
* The tri-state of a branch, from the checked set.
|
|
55
|
+
*
|
|
56
|
+
* A leaf is its own only leaf, so a caller can ask this of any node and get an
|
|
57
|
+
* answer that matches what a click on it will do. A branch whose leaves are all
|
|
58
|
+
* disabled has none to count and reports 'none': `leaves.every(isChecked)` is
|
|
59
|
+
* true over an empty array, which would draw that branch as fully checked and
|
|
60
|
+
* make the next click on it clear rows nobody chose.
|
|
61
|
+
*/
|
|
62
|
+
export declare function branchState<T>(node: TreeNode<T>, checked: ReadonlySet<string>): TriState;
|
|
63
|
+
/**
|
|
64
|
+
* The ids to add and remove to set a whole subtree.
|
|
65
|
+
*
|
|
66
|
+
* Only leaf ids are ever returned. A branch's state is derived from its leaves
|
|
67
|
+
* on every render, so storing the branch as well gives the two a way to
|
|
68
|
+
* disagree, and the stored one always wins on the next paint.
|
|
69
|
+
*
|
|
70
|
+
* The direction is decided by counting the same list the write is filtered
|
|
71
|
+
* from, which is what keeps a partly disabled branch honest: the state that
|
|
72
|
+
* says 'fill' cannot have counted a leaf the fill will skip.
|
|
73
|
+
*/
|
|
74
|
+
export declare function toggleSubtree<T>(node: TreeNode<T>, checked: ReadonlySet<string>): {
|
|
75
|
+
add: string[];
|
|
76
|
+
remove: string[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The visible rows, in order, with the depth and parent each one needs.
|
|
80
|
+
*
|
|
81
|
+
* A collapsed branch contributes its own row and nothing below it. The arrow
|
|
82
|
+
* keys walk this list, so a row that is not in it cannot be reached by keyboard
|
|
83
|
+
* either, which is the only way the keyboard order and the painted order stay
|
|
84
|
+
* the same list.
|
|
85
|
+
*/
|
|
86
|
+
export declare function flattenTree<T>(nodes: TreeNode<T>[], expanded: ReadonlySet<string>): TreeRow<T>[];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a data tree and the arithmetic a checkable one needs.
|
|
3
|
+
*
|
|
4
|
+
* A data tree is not a navigation menu. A menu shows where you can go and one
|
|
5
|
+
* item is current; a tree shows a structure, and a branch in it summarises
|
|
6
|
+
* everything beneath it while a click on that branch writes every one of them.
|
|
7
|
+
* Those two jobs are the reason this module exists: the summary and the write
|
|
8
|
+
* have to be computed from the same list, or a branch reports a state its own
|
|
9
|
+
* click does not produce.
|
|
10
|
+
*
|
|
11
|
+
* The tri-state itself comes from rollup.ts rather than being written again
|
|
12
|
+
* here. That module already holds the case a fold gets wrong, and a second
|
|
13
|
+
* implementation of it would be a second chance to get it wrong.
|
|
14
|
+
*
|
|
15
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
16
|
+
*/
|
|
17
|
+
import { nextState, rollUp, setSubtree } from './rollup.js';
|
|
18
|
+
/**
|
|
19
|
+
* Whether a node has anything to descend into.
|
|
20
|
+
*
|
|
21
|
+
* An empty `children` array is a leaf, not a branch that happens to be empty.
|
|
22
|
+
* A node that announces `aria-expanded` and then opens onto nothing tells a
|
|
23
|
+
* screen reader user there is more to read when there is not.
|
|
24
|
+
*/
|
|
25
|
+
export function isBranch(node) {
|
|
26
|
+
return (node.children?.length ?? 0) > 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every leaf under a node, disabled ones included, so the write and the count
|
|
30
|
+
* can be taken from one list.
|
|
31
|
+
*
|
|
32
|
+
* The walk stops at a disabled branch. Nothing in the subtree below it can be
|
|
33
|
+
* reached from any ancestor's control, so counting those leaves would describe
|
|
34
|
+
* rows the write skips.
|
|
35
|
+
*/
|
|
36
|
+
function walkLeaves(node, out) {
|
|
37
|
+
if (!isBranch(node)) {
|
|
38
|
+
out.push(node);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (node.disabled)
|
|
42
|
+
return;
|
|
43
|
+
for (const child of node.children ?? [])
|
|
44
|
+
walkLeaves(child, out);
|
|
45
|
+
}
|
|
46
|
+
/** Every leaf under a node, excluding disabled ones, because a disabled leaf must be neither written nor counted. */
|
|
47
|
+
export function leavesOf(node) {
|
|
48
|
+
const reachable = [];
|
|
49
|
+
walkLeaves(node, reachable);
|
|
50
|
+
return reachable.filter((leaf) => !leaf.disabled);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The tri-state of a branch, from the checked set.
|
|
54
|
+
*
|
|
55
|
+
* A leaf is its own only leaf, so a caller can ask this of any node and get an
|
|
56
|
+
* answer that matches what a click on it will do. A branch whose leaves are all
|
|
57
|
+
* disabled has none to count and reports 'none': `leaves.every(isChecked)` is
|
|
58
|
+
* true over an empty array, which would draw that branch as fully checked and
|
|
59
|
+
* make the next click on it clear rows nobody chose.
|
|
60
|
+
*/
|
|
61
|
+
export function branchState(node, checked) {
|
|
62
|
+
return rollUp(leavesOf(node), (leaf) => checked.has(leaf.id));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The ids to add and remove to set a whole subtree.
|
|
66
|
+
*
|
|
67
|
+
* Only leaf ids are ever returned. A branch's state is derived from its leaves
|
|
68
|
+
* on every render, so storing the branch as well gives the two a way to
|
|
69
|
+
* disagree, and the stored one always wins on the next paint.
|
|
70
|
+
*
|
|
71
|
+
* The direction is decided by counting the same list the write is filtered
|
|
72
|
+
* from, which is what keeps a partly disabled branch honest: the state that
|
|
73
|
+
* says 'fill' cannot have counted a leaf the fill will skip.
|
|
74
|
+
*/
|
|
75
|
+
export function toggleSubtree(node, checked) {
|
|
76
|
+
const reachable = [];
|
|
77
|
+
walkLeaves(node, reachable);
|
|
78
|
+
const settable = (leaf) => !leaf.disabled;
|
|
79
|
+
const value = nextState(rollUp(reachable.filter(settable), (leaf) => checked.has(leaf.id)));
|
|
80
|
+
const add = [];
|
|
81
|
+
const remove = [];
|
|
82
|
+
for (const write of setSubtree(reachable, value, settable)) {
|
|
83
|
+
const held = checked.has(write.item.id);
|
|
84
|
+
if (write.value && !held)
|
|
85
|
+
add.push(write.item.id);
|
|
86
|
+
if (!write.value && held)
|
|
87
|
+
remove.push(write.item.id);
|
|
88
|
+
}
|
|
89
|
+
return { add, remove };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The visible rows, in order, with the depth and parent each one needs.
|
|
93
|
+
*
|
|
94
|
+
* A collapsed branch contributes its own row and nothing below it. The arrow
|
|
95
|
+
* keys walk this list, so a row that is not in it cannot be reached by keyboard
|
|
96
|
+
* either, which is the only way the keyboard order and the painted order stay
|
|
97
|
+
* the same list.
|
|
98
|
+
*/
|
|
99
|
+
export function flattenTree(nodes, expanded) {
|
|
100
|
+
const rows = [];
|
|
101
|
+
const visit = (list, depth, parentId) => {
|
|
102
|
+
for (const node of list) {
|
|
103
|
+
rows.push(parentId === undefined ? { node, depth } : { node, depth, parentId });
|
|
104
|
+
if (isBranch(node) && expanded.has(node.id)) {
|
|
105
|
+
visit(node.children ?? [], depth + 1, node.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
visit(nodes, 0);
|
|
110
|
+
return rows;
|
|
111
|
+
}
|