@lyeve-labs/ui-kit 0.12.1 → 0.13.1
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/dist/components/Autocomplete.svelte +191 -125
- package/dist/components/Autocomplete.svelte.d.ts +29 -8
- package/dist/components/Card.svelte +43 -2
- package/dist/components/Card.svelte.d.ts +24 -2
- package/dist/components/Checkbox.svelte +174 -63
- 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/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/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 +43 -7
- 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 -35
- 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 +264 -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/Toolbar.svelte +39 -0
- package/dist/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/TreeView.svelte +339 -0
- package/dist/components/TreeView.svelte.d.ts +37 -0
- 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 +39 -0
- package/dist/internal/field.js +48 -0
- 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/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 +18 -10
- package/package.json +4 -2
- package/src/lib/styles/theme.css +18 -10
|
@@ -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
|
+
}
|
package/dist/styles/theme.css
CHANGED
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
* Caption 13px 1.50 0 400 Labels, metadata *
|
|
98
98
|
* Mono 18px 1.50 0 500 Code, eyebrows *
|
|
99
99
|
* */
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
--
|
|
106
|
-
--
|
|
100
|
+
--text-display: 4rem; /* 64px */
|
|
101
|
+
--text-h1: 2.75rem; /* 44px */
|
|
102
|
+
--text-h2: 2rem; /* 32px */
|
|
103
|
+
--text-h3: 1.375rem; /* 22px */
|
|
104
|
+
--text-body: 1rem; /* 16px */
|
|
105
|
+
--text-caption: 0.8125rem; /* 13px */
|
|
106
|
+
--text-mono: 1.125rem; /* 18px */
|
|
107
107
|
|
|
108
108
|
--leading-display: 1.05;
|
|
109
109
|
--leading-h1: 1.1;
|
|
@@ -122,16 +122,24 @@
|
|
|
122
122
|
* Tailwind's default 0.25rem (4px) grid is unchanged. *
|
|
123
123
|
* These tokens add named slots for common component spacing. */
|
|
124
124
|
--spacing-page-x: 1.5rem; /* 24px - page horizontal gutter */
|
|
125
|
-
--spacing-page-y:
|
|
126
|
-
|
|
125
|
+
--spacing-page-y: 1.5rem; /* 24px - page vertical gutter. Equal to page-x because no page
|
|
126
|
+
* measured renders an asymmetric gutter; the two names stay
|
|
127
|
+
* separate so one axis can move without dragging the other. */
|
|
128
|
+
--spacing-card: 1.25rem; /* 20px - card inner padding. 24px matched none of the card
|
|
129
|
+
* surfaces in use; 20px is their measured mode and Card's own md. */
|
|
127
130
|
--spacing-card-sm: 1rem; /* 16px - compact card padding */
|
|
128
|
-
--spacing-section:
|
|
131
|
+
--spacing-section: 2rem; /* 32px - section gap. 48px matched nothing anywhere; 32px is the
|
|
132
|
+
* gap the pages that set one already use, and PageHeader's own. */
|
|
129
133
|
--spacing-input-x: 0.75rem; /* 12px - input horizontal padding */
|
|
130
134
|
--spacing-input-y: 0.5rem; /* 8px - input vertical padding */
|
|
131
135
|
/* 38px - the height of every single-line control. Stated once so an Input,
|
|
132
136
|
* a Select and a NumberInput sitting in the same row line up. Derived from
|
|
133
137
|
* the 13px/1.25 control text plus input-y padding and a 1px border. */
|
|
134
138
|
--spacing-control: 2.375rem;
|
|
139
|
+
--spacing-panel-max: 15rem; /* 240px - the scroll cap on a floating option list */
|
|
140
|
+
--spacing-nav-indent: 0.875rem; /* 14px - one level of sidebar nesting */
|
|
141
|
+
--spacing-nav-rail: 3.5rem; /* 56px - the collapsed icon rail */
|
|
142
|
+
--spacing-sidebar: 14rem; /* 224px - the expanded sidebar */
|
|
135
143
|
--spacing-stack: 1rem; /* 16px - default vertical stack gap */
|
|
136
144
|
--spacing-inline: 0.5rem; /* 8px - default inline gap */
|
|
137
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyeve-labs/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "LyEve Labs <hello@lyeve.com>",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"type": "module",
|
|
25
25
|
"sideEffects": false,
|
|
26
|
-
"packageManager": "pnpm@
|
|
26
|
+
"packageManager": "pnpm@11.25.0",
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=20"
|
|
29
29
|
},
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
38
38
|
"test": "vitest run",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
|
+
"test:coverage": "vitest run --coverage",
|
|
41
|
+
"test:ct": "playwright test --config playwright-ct.config.ts",
|
|
40
42
|
"format": "prettier --write .",
|
|
41
43
|
"format:check": "prettier --check ."
|
|
42
44
|
},
|
package/src/lib/styles/theme.css
CHANGED
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
* Caption 13px 1.50 0 400 Labels, metadata *
|
|
98
98
|
* Mono 18px 1.50 0 500 Code, eyebrows *
|
|
99
99
|
* */
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
--
|
|
106
|
-
--
|
|
100
|
+
--text-display: 4rem; /* 64px */
|
|
101
|
+
--text-h1: 2.75rem; /* 44px */
|
|
102
|
+
--text-h2: 2rem; /* 32px */
|
|
103
|
+
--text-h3: 1.375rem; /* 22px */
|
|
104
|
+
--text-body: 1rem; /* 16px */
|
|
105
|
+
--text-caption: 0.8125rem; /* 13px */
|
|
106
|
+
--text-mono: 1.125rem; /* 18px */
|
|
107
107
|
|
|
108
108
|
--leading-display: 1.05;
|
|
109
109
|
--leading-h1: 1.1;
|
|
@@ -122,16 +122,24 @@
|
|
|
122
122
|
* Tailwind's default 0.25rem (4px) grid is unchanged. *
|
|
123
123
|
* These tokens add named slots for common component spacing. */
|
|
124
124
|
--spacing-page-x: 1.5rem; /* 24px - page horizontal gutter */
|
|
125
|
-
--spacing-page-y:
|
|
126
|
-
|
|
125
|
+
--spacing-page-y: 1.5rem; /* 24px - page vertical gutter. Equal to page-x because no page
|
|
126
|
+
* measured renders an asymmetric gutter; the two names stay
|
|
127
|
+
* separate so one axis can move without dragging the other. */
|
|
128
|
+
--spacing-card: 1.25rem; /* 20px - card inner padding. 24px matched none of the card
|
|
129
|
+
* surfaces in use; 20px is their measured mode and Card's own md. */
|
|
127
130
|
--spacing-card-sm: 1rem; /* 16px - compact card padding */
|
|
128
|
-
--spacing-section:
|
|
131
|
+
--spacing-section: 2rem; /* 32px - section gap. 48px matched nothing anywhere; 32px is the
|
|
132
|
+
* gap the pages that set one already use, and PageHeader's own. */
|
|
129
133
|
--spacing-input-x: 0.75rem; /* 12px - input horizontal padding */
|
|
130
134
|
--spacing-input-y: 0.5rem; /* 8px - input vertical padding */
|
|
131
135
|
/* 38px - the height of every single-line control. Stated once so an Input,
|
|
132
136
|
* a Select and a NumberInput sitting in the same row line up. Derived from
|
|
133
137
|
* the 13px/1.25 control text plus input-y padding and a 1px border. */
|
|
134
138
|
--spacing-control: 2.375rem;
|
|
139
|
+
--spacing-panel-max: 15rem; /* 240px - the scroll cap on a floating option list */
|
|
140
|
+
--spacing-nav-indent: 0.875rem; /* 14px - one level of sidebar nesting */
|
|
141
|
+
--spacing-nav-rail: 3.5rem; /* 56px - the collapsed icon rail */
|
|
142
|
+
--spacing-sidebar: 14rem; /* 224px - the expanded sidebar */
|
|
135
143
|
--spacing-stack: 1rem; /* 16px - default vertical stack gap */
|
|
136
144
|
--spacing-inline: 0.5rem; /* 8px - default inline gap */
|
|
137
145
|
}
|