@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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a sidebar decides which of its links is the current page.
|
|
3
|
+
*
|
|
4
|
+
* The rule every app shell reaches for is
|
|
5
|
+
*
|
|
6
|
+
* pathname === href || pathname.startsWith(href + '/')
|
|
7
|
+
*
|
|
8
|
+
* applied to every entry in the list. It is wrong in two ways that only show
|
|
9
|
+
* up once the list has more than one level. A parent link keeps claiming the
|
|
10
|
+
* page while the reader is on one of its children, so a settings sub-page
|
|
11
|
+
* lights two rows at once and neither of them is where the reader is. And two
|
|
12
|
+
* leaves where one href is a prefix of the other, /settings beside
|
|
13
|
+
* /settings/team, mark themselves together for the same reason, even though
|
|
14
|
+
* they are siblings and only one of them can be open.
|
|
15
|
+
*
|
|
16
|
+
* The fix is that prefix matching is a property of a node that owns a section,
|
|
17
|
+
* not of every node. A leaf answers for its own path and nothing below it; a
|
|
18
|
+
* node with children answers for its whole subtree, because that is what makes
|
|
19
|
+
* an ancestor able to say "you are somewhere in here" while its child says
|
|
20
|
+
* "you are here". Either default is overridable per node, and 'none' opts a
|
|
21
|
+
* node out of path matching entirely.
|
|
22
|
+
*
|
|
23
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Trailing slashes are noise: a router hands over /settings on one route and
|
|
27
|
+
* /settings/ on another, and the two name the same page. Everything else is
|
|
28
|
+
* left alone, because activePath is documented as a pathname and a query or a
|
|
29
|
+
* hash that reached here is a caller bug worth seeing rather than absorbing.
|
|
30
|
+
*/
|
|
31
|
+
function normalize(path) {
|
|
32
|
+
if (path === '')
|
|
33
|
+
return '';
|
|
34
|
+
const trimmed = path.replace(/\/+$/, '');
|
|
35
|
+
return trimmed === '' ? '/' : trimmed;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A node with children owns a section, so it answers for everything under it.
|
|
39
|
+
* A leaf answers for its own path alone.
|
|
40
|
+
*/
|
|
41
|
+
function matchMode(node) {
|
|
42
|
+
if (node.match)
|
|
43
|
+
return node.match;
|
|
44
|
+
return node.children && node.children.length > 0 ? 'prefix' : 'exact';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when this node is the current page.
|
|
48
|
+
*
|
|
49
|
+
* Path only. A disabled node still reports the truth about its href; whether
|
|
50
|
+
* it is rendered as a link is a separate decision the component makes.
|
|
51
|
+
*/
|
|
52
|
+
export function isActive(node, activePath) {
|
|
53
|
+
if (!node.href)
|
|
54
|
+
return false;
|
|
55
|
+
const mode = matchMode(node);
|
|
56
|
+
if (mode === 'none')
|
|
57
|
+
return false;
|
|
58
|
+
const href = normalize(node.href);
|
|
59
|
+
const path = normalize(activePath);
|
|
60
|
+
if (href === '' || path === '')
|
|
61
|
+
return false;
|
|
62
|
+
if (path === href)
|
|
63
|
+
return true;
|
|
64
|
+
// A root href normalizes to "/", so the prefix it tests for is "//" and it
|
|
65
|
+
// claims nothing but itself. A dashboard mounted at / would otherwise own
|
|
66
|
+
// every page in the app.
|
|
67
|
+
return mode === 'prefix' && path.startsWith(`${href}/`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The ids from the root down to the matched leaf, so its ancestors can open
|
|
71
|
+
* and mark themselves.
|
|
72
|
+
*
|
|
73
|
+
* Children are searched before the node itself, so a group never shadows the
|
|
74
|
+
* child that holds the more specific answer. The first match in document order
|
|
75
|
+
* wins: two nodes claiming one path is a tree the author has to fix, and
|
|
76
|
+
* silently picking one of them by length would hide it.
|
|
77
|
+
*/
|
|
78
|
+
export function activeTrail(items, activePath) {
|
|
79
|
+
for (const node of items) {
|
|
80
|
+
if (node.children && node.children.length > 0) {
|
|
81
|
+
const below = activeTrail(node.children, activePath);
|
|
82
|
+
if (below.length > 0)
|
|
83
|
+
return [node.id, ...below];
|
|
84
|
+
}
|
|
85
|
+
if (isActive(node, activePath))
|
|
86
|
+
return [node.id];
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
/** Depth-first flatten, for tests and for keyboard order. */
|
|
91
|
+
export function flattenNav(items) {
|
|
92
|
+
const out = [];
|
|
93
|
+
const walk = (nodes, depth) => {
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
out.push({ node, depth });
|
|
96
|
+
if (node.children && node.children.length > 0)
|
|
97
|
+
walk(node.children, depth + 1);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(items, 0);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for how a floating panel looks.
|
|
3
|
+
*
|
|
4
|
+
* field.ts states the resting control and nothing stated the surface that
|
|
5
|
+
* opens above it, so each of the four popovers grew its own:
|
|
6
|
+
*
|
|
7
|
+
* MultiSelect absolute z-50 mt-1 w-full rounded-xl border border-line
|
|
8
|
+
* bg-surface shadow-2xl overflow-hidden
|
|
9
|
+
* and, on an inner region, max-h-60 overflow-y-auto py-1
|
|
10
|
+
* Autocomplete absolute z-50 mt-1 w-full max-h-60 overflow-y-auto
|
|
11
|
+
* rounded-xl border border-line bg-surface shadow-2xl py-1
|
|
12
|
+
* DatePicker absolute z-50 mt-1 w-[17rem] rounded-xl border border-line
|
|
13
|
+
* bg-surface shadow-2xl p-3
|
|
14
|
+
* Dropdown absolute z-50 mt-1 py-1 min-w-36 rounded-xl border
|
|
15
|
+
* border-line bg-surface shadow-2xl
|
|
16
|
+
*
|
|
17
|
+
* Three of those set a width the consumer cannot influence, and DatePicker
|
|
18
|
+
* sets it as w-[17rem], a length no token governs. The scroll lives on an
|
|
19
|
+
* inner region in one, on the surface itself in another and nowhere in the
|
|
20
|
+
* other two, so a Dropdown of eighty items runs past the bottom of the window
|
|
21
|
+
* and the last item cannot be reached. Two panels cap their height at max-h-60
|
|
22
|
+
* and two never cap it. All four draw the boundary with border-line, which
|
|
23
|
+
* reads 1.25:1 and is the only thing separating the panel from whatever it
|
|
24
|
+
* happens to float over. The shadow is the one thing they agreed on.
|
|
25
|
+
*
|
|
26
|
+
* Width stays at the call site: a menu sized to its trigger and a calendar
|
|
27
|
+
* sized to seven columns are different requirements. Everything else is here.
|
|
28
|
+
*
|
|
29
|
+
* One rule holds these together. No row carries two utilities for the same
|
|
30
|
+
* property. Colour and background sit on the surface, and a row states only
|
|
31
|
+
* the override its state earns, because two utilities for one property resolve
|
|
32
|
+
* in the order Tailwind emits them and not in the order they were written.
|
|
33
|
+
*
|
|
34
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The floating surface every popover paints.
|
|
38
|
+
*
|
|
39
|
+
* border-line-strong, not border-line: a panel floating over arbitrary content
|
|
40
|
+
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
|
+
* resting text colour so a row can override it with a single utility.
|
|
42
|
+
*/
|
|
43
|
+
export declare const PANEL_SURFACE = "absolute z-50 mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
|
|
44
|
+
/**
|
|
45
|
+
* The scrolling region inside it.
|
|
46
|
+
*
|
|
47
|
+
* The cap is a token, not max-h-60, so a fifth panel cannot pick a different
|
|
48
|
+
* one. overscroll-contain stops a wheel that has reached the end of the list
|
|
49
|
+
* from carrying on into the page behind the open panel.
|
|
50
|
+
*/
|
|
51
|
+
export declare const PANEL_LIST = "max-h-panel-max overflow-y-auto overscroll-contain py-1";
|
|
52
|
+
/**
|
|
53
|
+
* One row at rest.
|
|
54
|
+
*
|
|
55
|
+
* No background and no text colour of its own: it inherits both from the
|
|
56
|
+
* surface, which leaves each state below a single utility to override.
|
|
57
|
+
*/
|
|
58
|
+
export declare const PANEL_OPTION: string;
|
|
59
|
+
/**
|
|
60
|
+
* The active descendant.
|
|
61
|
+
*
|
|
62
|
+
* A tint alone reads 1.09:1 against the panel, which is not a visible state,
|
|
63
|
+
* so the active row also carries an inset brand ring. Inset because the row
|
|
64
|
+
* spans the full width of the panel and an outset ring is clipped by it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const PANEL_OPTION_ACTIVE = "bg-surface-2 ring-1 ring-inset ring-brand";
|
|
67
|
+
/**
|
|
68
|
+
* A row whose value is selected, which is orthogonal to being active.
|
|
69
|
+
*
|
|
70
|
+
* The keyboard sits on one row while any number of rows are chosen, so this
|
|
71
|
+
* changes the text and never the background the active row is using.
|
|
72
|
+
*/
|
|
73
|
+
export declare const PANEL_OPTION_SELECTED = "font-medium text-brand";
|
|
74
|
+
/**
|
|
75
|
+
* A row that cannot be chosen.
|
|
76
|
+
*
|
|
77
|
+
* pointer-events-none rather than a hover override: :hover still matches a
|
|
78
|
+
* disabled button, and a second hover background on the same row would resolve
|
|
79
|
+
* by emitted order, so the row could take the tint and read as choosable.
|
|
80
|
+
*/
|
|
81
|
+
export declare const PANEL_OPTION_DISABLED = "pointer-events-none opacity-40";
|
|
82
|
+
/** The line shown when a filter matched nothing. */
|
|
83
|
+
export declare const PANEL_EMPTY = "px-3 py-2 text-sm text-faint";
|
|
84
|
+
/** A group heading inside the list. Not focusable, so it takes no row classes. */
|
|
85
|
+
export declare const PANEL_GROUP_LABEL = "px-3 pb-1 pt-3 text-xs font-medium uppercase text-faint";
|
|
86
|
+
/**
|
|
87
|
+
* Composes the option classes for a row's state.
|
|
88
|
+
*
|
|
89
|
+
* Exists as a function because three booleans spelled inline at each call site
|
|
90
|
+
* is how the four panels drifted. A disabled row takes the disabled treatment
|
|
91
|
+
* and never the active one, even while the keyboard is resting on it: painting
|
|
92
|
+
* it as the active descendant says Enter will choose it, and Enter will not.
|
|
93
|
+
* Selection survives both, because a chosen row that has since been disabled
|
|
94
|
+
* is still chosen.
|
|
95
|
+
*/
|
|
96
|
+
export declare function panelOption(state: {
|
|
97
|
+
active: boolean;
|
|
98
|
+
selected: boolean;
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
}): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for how a floating panel looks.
|
|
3
|
+
*
|
|
4
|
+
* field.ts states the resting control and nothing stated the surface that
|
|
5
|
+
* opens above it, so each of the four popovers grew its own:
|
|
6
|
+
*
|
|
7
|
+
* MultiSelect absolute z-50 mt-1 w-full rounded-xl border border-line
|
|
8
|
+
* bg-surface shadow-2xl overflow-hidden
|
|
9
|
+
* and, on an inner region, max-h-60 overflow-y-auto py-1
|
|
10
|
+
* Autocomplete absolute z-50 mt-1 w-full max-h-60 overflow-y-auto
|
|
11
|
+
* rounded-xl border border-line bg-surface shadow-2xl py-1
|
|
12
|
+
* DatePicker absolute z-50 mt-1 w-[17rem] rounded-xl border border-line
|
|
13
|
+
* bg-surface shadow-2xl p-3
|
|
14
|
+
* Dropdown absolute z-50 mt-1 py-1 min-w-36 rounded-xl border
|
|
15
|
+
* border-line bg-surface shadow-2xl
|
|
16
|
+
*
|
|
17
|
+
* Three of those set a width the consumer cannot influence, and DatePicker
|
|
18
|
+
* sets it as w-[17rem], a length no token governs. The scroll lives on an
|
|
19
|
+
* inner region in one, on the surface itself in another and nowhere in the
|
|
20
|
+
* other two, so a Dropdown of eighty items runs past the bottom of the window
|
|
21
|
+
* and the last item cannot be reached. Two panels cap their height at max-h-60
|
|
22
|
+
* and two never cap it. All four draw the boundary with border-line, which
|
|
23
|
+
* reads 1.25:1 and is the only thing separating the panel from whatever it
|
|
24
|
+
* happens to float over. The shadow is the one thing they agreed on.
|
|
25
|
+
*
|
|
26
|
+
* Width stays at the call site: a menu sized to its trigger and a calendar
|
|
27
|
+
* sized to seven columns are different requirements. Everything else is here.
|
|
28
|
+
*
|
|
29
|
+
* One rule holds these together. No row carries two utilities for the same
|
|
30
|
+
* property. Colour and background sit on the surface, and a row states only
|
|
31
|
+
* the override its state earns, because two utilities for one property resolve
|
|
32
|
+
* in the order Tailwind emits them and not in the order they were written.
|
|
33
|
+
*
|
|
34
|
+
* Not exported from the package entry point - this is an implementation detail.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The floating surface every popover paints.
|
|
38
|
+
*
|
|
39
|
+
* border-line-strong, not border-line: a panel floating over arbitrary content
|
|
40
|
+
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
|
+
* resting text colour so a row can override it with a single utility.
|
|
42
|
+
*/
|
|
43
|
+
export const PANEL_SURFACE = 'absolute z-50 mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
|
|
44
|
+
/**
|
|
45
|
+
* The scrolling region inside it.
|
|
46
|
+
*
|
|
47
|
+
* The cap is a token, not max-h-60, so a fifth panel cannot pick a different
|
|
48
|
+
* one. overscroll-contain stops a wheel that has reached the end of the list
|
|
49
|
+
* from carrying on into the page behind the open panel.
|
|
50
|
+
*/
|
|
51
|
+
export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain py-1';
|
|
52
|
+
/**
|
|
53
|
+
* One row at rest.
|
|
54
|
+
*
|
|
55
|
+
* No background and no text colour of its own: it inherits both from the
|
|
56
|
+
* surface, which leaves each state below a single utility to override.
|
|
57
|
+
*/
|
|
58
|
+
export const PANEL_OPTION = 'flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm ' +
|
|
59
|
+
'transition-colors duration-150 outline-none hover:bg-surface-2 ' +
|
|
60
|
+
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
61
|
+
/**
|
|
62
|
+
* The active descendant.
|
|
63
|
+
*
|
|
64
|
+
* A tint alone reads 1.09:1 against the panel, which is not a visible state,
|
|
65
|
+
* so the active row also carries an inset brand ring. Inset because the row
|
|
66
|
+
* spans the full width of the panel and an outset ring is clipped by it.
|
|
67
|
+
*/
|
|
68
|
+
export const PANEL_OPTION_ACTIVE = 'bg-surface-2 ring-1 ring-inset ring-brand';
|
|
69
|
+
/**
|
|
70
|
+
* A row whose value is selected, which is orthogonal to being active.
|
|
71
|
+
*
|
|
72
|
+
* The keyboard sits on one row while any number of rows are chosen, so this
|
|
73
|
+
* changes the text and never the background the active row is using.
|
|
74
|
+
*/
|
|
75
|
+
export const PANEL_OPTION_SELECTED = 'font-medium text-brand';
|
|
76
|
+
/**
|
|
77
|
+
* A row that cannot be chosen.
|
|
78
|
+
*
|
|
79
|
+
* pointer-events-none rather than a hover override: :hover still matches a
|
|
80
|
+
* disabled button, and a second hover background on the same row would resolve
|
|
81
|
+
* by emitted order, so the row could take the tint and read as choosable.
|
|
82
|
+
*/
|
|
83
|
+
export const PANEL_OPTION_DISABLED = 'pointer-events-none opacity-40';
|
|
84
|
+
/** The line shown when a filter matched nothing. */
|
|
85
|
+
export const PANEL_EMPTY = 'px-3 py-2 text-sm text-faint';
|
|
86
|
+
/** A group heading inside the list. Not focusable, so it takes no row classes. */
|
|
87
|
+
export const PANEL_GROUP_LABEL = 'px-3 pb-1 pt-3 text-xs font-medium uppercase text-faint';
|
|
88
|
+
/**
|
|
89
|
+
* Composes the option classes for a row's state.
|
|
90
|
+
*
|
|
91
|
+
* Exists as a function because three booleans spelled inline at each call site
|
|
92
|
+
* is how the four panels drifted. A disabled row takes the disabled treatment
|
|
93
|
+
* and never the active one, even while the keyboard is resting on it: painting
|
|
94
|
+
* it as the active descendant says Enter will choose it, and Enter will not.
|
|
95
|
+
* Selection survives both, because a chosen row that has since been disabled
|
|
96
|
+
* is still chosen.
|
|
97
|
+
*/
|
|
98
|
+
export function panelOption(state) {
|
|
99
|
+
const parts = [PANEL_OPTION];
|
|
100
|
+
if (state.selected)
|
|
101
|
+
parts.push(PANEL_OPTION_SELECTED);
|
|
102
|
+
if (state.disabled) {
|
|
103
|
+
parts.push(PANEL_OPTION_DISABLED);
|
|
104
|
+
}
|
|
105
|
+
else if (state.active) {
|
|
106
|
+
parts.push(PANEL_OPTION_ACTIVE);
|
|
107
|
+
}
|
|
108
|
+
return parts.join(' ');
|
|
109
|
+
}
|
|
@@ -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;
|