@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.
Files changed (101) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AccordionItem.svelte +1 -1
  3. package/dist/components/Autocomplete.svelte +191 -125
  4. package/dist/components/Autocomplete.svelte.d.ts +29 -8
  5. package/dist/components/Button.svelte +26 -4
  6. package/dist/components/Card.svelte +61 -3
  7. package/dist/components/Card.svelte.d.ts +24 -2
  8. package/dist/components/Checkbox.svelte +174 -59
  9. package/dist/components/Checkbox.svelte.d.ts +20 -3
  10. package/dist/components/CheckboxGroup.svelte +162 -0
  11. package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
  12. package/dist/components/Collapsible.svelte +142 -0
  13. package/dist/components/Collapsible.svelte.d.ts +32 -0
  14. package/dist/components/CopyButton.svelte +126 -0
  15. package/dist/components/CopyButton.svelte.d.ts +14 -0
  16. package/dist/components/DatePicker.svelte +48 -6
  17. package/dist/components/DateTimePicker.svelte +337 -0
  18. package/dist/components/DateTimePicker.svelte.d.ts +26 -0
  19. package/dist/components/DescriptionList.svelte +78 -0
  20. package/dist/components/DescriptionList.svelte.d.ts +34 -0
  21. package/dist/components/Drawer.svelte +15 -4
  22. package/dist/components/Field.svelte +104 -0
  23. package/dist/components/Field.svelte.d.ts +46 -0
  24. package/dist/components/FileInput.svelte +5 -2
  25. package/dist/components/FormMessage.svelte +85 -0
  26. package/dist/components/FormMessage.svelte.d.ts +11 -0
  27. package/dist/components/Input.svelte +1 -1
  28. package/dist/components/Label.svelte +7 -1
  29. package/dist/components/Label.svelte.d.ts +6 -0
  30. package/dist/components/Modal.svelte +25 -8
  31. package/dist/components/MultiSelect.svelte +199 -109
  32. package/dist/components/MultiSelect.svelte.d.ts +22 -9
  33. package/dist/components/NumberInput.svelte +8 -4
  34. package/dist/components/PageHeader.svelte +37 -4
  35. package/dist/components/PageHeader.svelte.d.ts +15 -0
  36. package/dist/components/PageShell.svelte +85 -0
  37. package/dist/components/PageShell.svelte.d.ts +38 -0
  38. package/dist/components/Pagination.svelte +58 -17
  39. package/dist/components/Panel.svelte +101 -0
  40. package/dist/components/Panel.svelte.d.ts +39 -0
  41. package/dist/components/PasswordInput.svelte +139 -0
  42. package/dist/components/PasswordInput.svelte.d.ts +29 -0
  43. package/dist/components/Radio.svelte +152 -32
  44. package/dist/components/Radio.svelte.d.ts +16 -1
  45. package/dist/components/RadioGroup.svelte +118 -71
  46. package/dist/components/RadioGroup.svelte.d.ts +39 -9
  47. package/dist/components/SectionHeading.svelte +39 -0
  48. package/dist/components/SectionHeading.svelte.d.ts +21 -0
  49. package/dist/components/SegmentedControl.svelte +194 -0
  50. package/dist/components/SegmentedControl.svelte.d.ts +55 -0
  51. package/dist/components/Select.svelte +471 -46
  52. package/dist/components/Select.svelte.d.ts +95 -6
  53. package/dist/components/SidebarNav.svelte +259 -0
  54. package/dist/components/SidebarNav.svelte.d.ts +17 -0
  55. package/dist/components/Stat.svelte +53 -2
  56. package/dist/components/Stat.svelte.d.ts +31 -0
  57. package/dist/components/Textarea.svelte +1 -1
  58. package/dist/components/TimePicker.svelte +480 -0
  59. package/dist/components/TimePicker.svelte.d.ts +23 -0
  60. package/dist/components/Toaster.svelte +9 -2
  61. package/dist/components/Toggle.svelte +5 -1
  62. package/dist/components/Toggle.svelte.d.ts +2 -0
  63. package/dist/components/Toolbar.svelte +39 -0
  64. package/dist/components/Toolbar.svelte.d.ts +26 -0
  65. package/dist/components/Tooltip.svelte +48 -12
  66. package/dist/components/TreeView.svelte +339 -0
  67. package/dist/components/TreeView.svelte.d.ts +37 -0
  68. package/dist/components/dialog/Dialog.svelte +15 -58
  69. package/dist/components/dialog/dialog-manager.svelte.d.ts +2 -2
  70. package/dist/components/dialog/dialog-manager.svelte.js +21 -21
  71. package/dist/index.d.ts +25 -1
  72. package/dist/index.js +20 -1
  73. package/dist/internal/calendar.d.ts +119 -0
  74. package/dist/internal/calendar.js +225 -0
  75. package/dist/internal/choice.d.ts +136 -0
  76. package/dist/internal/choice.js +179 -0
  77. package/dist/internal/field.d.ts +31 -0
  78. package/dist/internal/field.js +42 -1
  79. package/dist/internal/filter.d.ts +80 -0
  80. package/dist/internal/filter.js +80 -0
  81. package/dist/internal/layout.d.ts +119 -0
  82. package/dist/internal/layout.js +132 -0
  83. package/dist/internal/listbox.svelte.d.ts +77 -0
  84. package/dist/internal/listbox.svelte.js +438 -0
  85. package/dist/internal/nav-expansion.svelte.d.ts +36 -0
  86. package/dist/internal/nav-expansion.svelte.js +144 -0
  87. package/dist/internal/nav-tree.d.ts +68 -0
  88. package/dist/internal/nav-tree.js +102 -0
  89. package/dist/internal/overlay.d.ts +25 -0
  90. package/dist/internal/overlay.js +92 -0
  91. package/dist/internal/panel.d.ts +100 -0
  92. package/dist/internal/panel.js +109 -0
  93. package/dist/internal/rollup.d.ts +52 -0
  94. package/dist/internal/rollup.js +67 -0
  95. package/dist/internal/time.d.ts +103 -0
  96. package/dist/internal/time.js +166 -0
  97. package/dist/internal/tree.d.ts +86 -0
  98. package/dist/internal/tree.js +111 -0
  99. package/dist/styles/theme.css +66 -25
  100. package/package.json +4 -2
  101. 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
+ }