@getaster/elements 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +13 -4
- package/bin/aster-theme-check.js +35 -4
- package/dist/base/element.d.ts +8 -1
- package/dist/base/element.js +7 -2
- package/dist/button/button.d.ts +17 -2
- package/dist/button/button.js +42 -8
- package/dist/composition/builtins.d.ts +623 -1
- package/dist/composition/builtins.js +232 -4
- package/dist/date/calendar.d.ts +3 -0
- package/dist/date/calendar.js +8 -2
- package/dist/date/date-field.d.ts +23 -3
- package/dist/date/date-field.js +77 -9
- package/dist/date/dates.d.ts +10 -2
- package/dist/date/dates.js +23 -3
- package/dist/dialog/config.d.ts +4 -0
- package/dist/dialog/config.js +8 -0
- package/dist/dialog/dialog.d.ts +34 -6
- package/dist/dialog/dialog.js +151 -14
- package/dist/field/bound-field.d.ts +17 -1
- package/dist/field/bound-field.js +68 -7
- package/dist/field/check.js +1 -0
- package/dist/field/field.js +1 -1
- package/dist/field/input.d.ts +1 -0
- package/dist/field/input.js +2 -2
- package/dist/field/number-field.js +2 -2
- package/dist/filter/filter-builder.d.ts +7 -1
- package/dist/filter/filter-builder.js +8 -1
- package/dist/form/controls.d.ts +40 -8
- package/dist/form/controls.js +58 -15
- package/dist/form/custom-field.d.ts +1 -0
- package/dist/form/custom-field.js +23 -0
- package/dist/form/features.d.ts +27 -0
- package/dist/form/features.js +52 -0
- package/dist/form/form.d.ts +67 -7
- package/dist/form/form.js +267 -24
- package/dist/form/policy.d.ts +29 -0
- package/dist/form/policy.js +16 -0
- package/dist/grid/appearance.d.ts +365 -0
- package/dist/grid/appearance.js +814 -0
- package/dist/grid/features.d.ts +38 -0
- package/dist/grid/features.js +78 -0
- package/dist/grid/grid.d.ts +216 -4
- package/dist/grid/grid.js +1853 -301
- package/dist/grid/policy.d.ts +58 -0
- package/dist/grid/policy.js +17 -0
- package/dist/grid/view.d.ts +32 -2
- package/dist/grid/view.js +89 -4
- package/dist/index.d.ts +14 -11
- package/dist/index.js +2 -1
- package/dist/menu/config.d.ts +7 -0
- package/dist/menu/config.js +44 -0
- package/dist/menu/menu.d.ts +23 -0
- package/dist/menu/menu.js +69 -17
- package/dist/picker/picker.js +8 -3
- package/dist/section/accordion.d.ts +26 -0
- package/dist/section/accordion.js +86 -8
- package/dist/section/policy.d.ts +8 -0
- package/dist/section/policy.js +17 -0
- package/dist/section/section.d.ts +30 -0
- package/dist/section/section.js +106 -3
- package/dist/select/choice-element.d.ts +12 -0
- package/dist/select/choice-element.js +36 -0
- package/dist/select/choice.d.ts +12 -2
- package/dist/select/choice.js +93 -28
- package/dist/select/select.d.ts +12 -0
- package/dist/select/select.js +37 -1
- package/dist/styles/contract.d.ts +14 -0
- package/dist/styles/contract.js +15 -0
- package/dist/switch/switch.d.ts +16 -0
- package/dist/switch/switch.js +56 -6
- package/dist/tree/navigator.d.ts +3 -0
- package/dist/tree/navigator.js +26 -2
- package/dist/window/chrome.d.ts +5 -1
- package/dist/window/chrome.js +4 -3
- package/dist/window/config.d.ts +71 -0
- package/dist/window/config.js +40 -0
- package/dist/window/window.d.ts +51 -13
- package/dist/window/window.js +233 -38
- package/dist/yaml.d.ts +19 -0
- package/dist/yaml.js +57 -0
- package/package.json +10 -4
- package/styles/aster-ui.css +233 -10
- package/styles/base.css +36 -0
- package/styles/contract.ts +16 -0
- package/styles/elements.css +197 -10
- package/styles/fonts/OFL-bricolage-grotesque.txt +93 -0
- package/styles/fonts/OFL-fraunces.txt +93 -0
- package/styles/fonts/bricolage-grotesque-latin-opsz-normal.woff2 +0 -0
- package/styles/fonts/fraunces-latin-full-normal.woff2 +0 -0
- package/styles/fonts.css +17 -0
- package/styles/theme-harbour.css +5 -0
- package/styles/theme-lamplight.css +5 -0
package/dist/select/choice.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { signal } from "@preact/signals-core";
|
|
2
2
|
import { html, nothing } from "lit-html";
|
|
3
3
|
import { anchorStyle, openWhen, placeStyle } from "../overlay/anchor.js";
|
|
4
|
-
/** Whether a choice's list is open,
|
|
4
|
+
/** Whether a choice's list is open, which option is active in it, and the filter text. */
|
|
5
5
|
export class ChoiceState {
|
|
6
6
|
open = signal(false);
|
|
7
7
|
active = signal(0);
|
|
8
|
+
query = signal("");
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* A select-only combobox: the value in a frame, and a list drawn like the
|
|
@@ -12,51 +13,70 @@ export class ChoiceState {
|
|
|
12
13
|
*
|
|
13
14
|
* Keyboard: ↓/↑, Enter or Space open; in the list ↓/↑, Home and End move,
|
|
14
15
|
* a letter jumps to the next label starting with it, Enter or Space choose,
|
|
15
|
-
* Escape and Tab close.
|
|
16
|
+
* Escape and Tab close. With `filter`, typed text narrows the list and
|
|
17
|
+
* Backspace edits it; Enter chooses the active option. The active option is
|
|
18
|
+
* `aria-activedescendant`.
|
|
16
19
|
*/
|
|
17
20
|
export function choiceView(state, p) {
|
|
18
|
-
const
|
|
21
|
+
const locked = p.disabled === true;
|
|
22
|
+
const readOnly = p.readOnly === true;
|
|
23
|
+
const filter = p.filter === true;
|
|
24
|
+
const query = filter ? state.query.value : "";
|
|
25
|
+
const all = p.options;
|
|
26
|
+
const choices = filter && query.trim() !== "" ? all.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase())) : all;
|
|
19
27
|
const current = p.value ?? null;
|
|
28
|
+
const chosenAll = all.findIndex((o) => o.value === current);
|
|
20
29
|
const chosen = choices.findIndex((o) => o.value === current);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
30
|
+
const shownValue = chosenAll >= 0 ? all[chosenAll].label : current === null || current === "" ? p.placeholder : String(current);
|
|
31
|
+
const shown = filter && state.open.peek() && query !== "" ? query : shownValue;
|
|
32
|
+
const open = state.open.value && !readOnly && !locked;
|
|
33
|
+
const active = choices.length ? Math.min(state.active.value, choices.length - 1) : -1;
|
|
24
34
|
const listId = `${p.id}-options`;
|
|
25
35
|
const optionId = (i) => `${p.id}-option-${i}`;
|
|
36
|
+
const close = () => {
|
|
37
|
+
state.open.value = false;
|
|
38
|
+
state.query.value = "";
|
|
39
|
+
};
|
|
26
40
|
const choose = (i) => {
|
|
27
41
|
const o = choices[i];
|
|
28
42
|
if (o)
|
|
29
43
|
p.choose(o.value);
|
|
30
|
-
|
|
44
|
+
close();
|
|
31
45
|
};
|
|
32
46
|
const show = () => {
|
|
33
|
-
if (
|
|
47
|
+
if (readOnly || locked)
|
|
34
48
|
return;
|
|
35
49
|
state.active.value = Math.max(0, chosen);
|
|
50
|
+
state.query.value = "";
|
|
36
51
|
state.open.value = true;
|
|
37
52
|
};
|
|
53
|
+
const clearable = p.clearable === true && current !== null && current !== "" && !readOnly && !locked;
|
|
38
54
|
return html `<div class="aster-picker">
|
|
39
55
|
<div class="aster-field aster-select" data-ring-owner style=${anchorStyle(p.id)}
|
|
40
56
|
@click=${(e) => {
|
|
57
|
+
if (readOnly || locked)
|
|
58
|
+
return;
|
|
41
59
|
e.currentTarget.querySelector("[role=combobox]")?.focus();
|
|
42
60
|
if (state.open.peek())
|
|
43
|
-
|
|
61
|
+
close();
|
|
44
62
|
else
|
|
45
63
|
show();
|
|
46
64
|
}}>
|
|
47
|
-
<div id=${p.id} class="aster-select-value" role="combobox" tabindex
|
|
65
|
+
<div id=${p.id} class="aster-select-value" role="combobox" tabindex=${locked ? "-1" : "0"}
|
|
48
66
|
aria-label=${p.label ?? nothing}
|
|
49
67
|
aria-labelledby=${p.labelledBy ?? nothing}
|
|
50
68
|
aria-haspopup="listbox" aria-expanded=${open ? "true" : "false"} aria-controls=${listId}
|
|
51
|
-
aria-activedescendant=${open ? optionId(active) : nothing}
|
|
52
|
-
aria-
|
|
69
|
+
aria-activedescendant=${open && active >= 0 ? optionId(active) : nothing}
|
|
70
|
+
aria-autocomplete=${filter ? "list" : nothing}
|
|
71
|
+
aria-readonly=${readOnly ? "true" : nothing}
|
|
72
|
+
aria-disabled=${locked ? "true" : nothing}
|
|
53
73
|
aria-required=${p.required ? "true" : nothing}
|
|
54
74
|
aria-invalid=${p.invalid ? "true" : nothing}
|
|
55
75
|
aria-describedby=${p.describedBy ?? nothing}
|
|
56
|
-
?data-placeholder=${
|
|
57
|
-
@blur=${() => (
|
|
76
|
+
?data-placeholder=${chosenAll < 0 && !(filter && query !== "")}
|
|
77
|
+
@blur=${() => close()}
|
|
58
78
|
@keydown=${(e) => {
|
|
59
|
-
if (
|
|
79
|
+
if (readOnly || locked)
|
|
60
80
|
return;
|
|
61
81
|
const n = choices.length;
|
|
62
82
|
if (!state.open.peek()) {
|
|
@@ -64,6 +84,18 @@ export function choiceView(state, p) {
|
|
|
64
84
|
e.preventDefault();
|
|
65
85
|
show();
|
|
66
86
|
}
|
|
87
|
+
else if (filter && isTyping(e)) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
state.query.value = e.key;
|
|
90
|
+
state.active.value = 0;
|
|
91
|
+
state.open.value = true;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (filter && isTyping(e)) {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
state.query.value = query + e.key;
|
|
98
|
+
state.active.value = 0;
|
|
67
99
|
return;
|
|
68
100
|
}
|
|
69
101
|
const move = (i) => {
|
|
@@ -80,19 +112,34 @@ export function choiceView(state, p) {
|
|
|
80
112
|
case "End":
|
|
81
113
|
return move(n - 1);
|
|
82
114
|
case "Enter":
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
return choose(active);
|
|
83
117
|
case " ":
|
|
118
|
+
if (filter) {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
state.query.value = query + e.key;
|
|
121
|
+
state.active.value = 0;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
84
124
|
e.preventDefault();
|
|
85
125
|
return choose(active);
|
|
126
|
+
case "Backspace":
|
|
127
|
+
if (!filter)
|
|
128
|
+
return;
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
state.query.value = query.slice(0, -1);
|
|
131
|
+
state.active.value = 0;
|
|
132
|
+
return;
|
|
86
133
|
case "Escape":
|
|
87
134
|
e.preventDefault();
|
|
88
135
|
e.stopPropagation();
|
|
89
|
-
|
|
136
|
+
close();
|
|
90
137
|
return;
|
|
91
138
|
case "Tab":
|
|
92
|
-
|
|
139
|
+
close();
|
|
93
140
|
return;
|
|
94
141
|
default: {
|
|
95
|
-
if (
|
|
142
|
+
if (filter || !isTyping(e))
|
|
96
143
|
return;
|
|
97
144
|
const k = e.key.toLowerCase();
|
|
98
145
|
for (let step = 1; step <= n; step++) {
|
|
@@ -103,20 +150,38 @@ export function choiceView(state, p) {
|
|
|
103
150
|
}
|
|
104
151
|
}
|
|
105
152
|
}}>${shown}</div>
|
|
153
|
+
${clearable
|
|
154
|
+
? html `<button type="button" class="aster-select-clear" aria-label=${`Clear ${p.label || "selection"}`}
|
|
155
|
+
@click=${(e) => {
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
close();
|
|
158
|
+
p.choose(null);
|
|
159
|
+
const frame = e.currentTarget.closest(".aster-select");
|
|
160
|
+
queueMicrotask(() => frame?.querySelector("[role=combobox]")?.focus());
|
|
161
|
+
}}>
|
|
162
|
+
<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 3l6 6M9 3l-6 6"/></svg>
|
|
163
|
+
</button>`
|
|
164
|
+
: nothing}
|
|
106
165
|
<svg class="aster-picker-chevron" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 3.8 5 6.8l3-3"/></svg>
|
|
107
166
|
</div>
|
|
108
167
|
<div class="aster-picker-list aster-popover" popover="manual" style=${placeStyle(p.id)} ${openWhen(open)}
|
|
109
168
|
id=${listId} role="listbox" aria-label=${p.label ?? nothing} aria-labelledby=${p.labelledBy ?? nothing}>
|
|
110
|
-
${choices.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
${choices.length === 0 && filter && query !== ""
|
|
170
|
+
? html `<div class="aster-choice-empty" role="status">${p.noResults ?? "No matches"}</div>`
|
|
171
|
+
: choices.map((o, i) => html `<div class="aster-option" role="option" id=${optionId(i)}
|
|
172
|
+
aria-selected=${i === chosen ? "true" : "false"} ?data-active=${i === active}
|
|
173
|
+
@pointerdown=${(ev) => ev.preventDefault()}
|
|
174
|
+
@pointerup=${(ev) => {
|
|
175
|
+
// WebKit suppresses touch clicks after pointerdown is cancelled to retain focus.
|
|
176
|
+
if (ev.pointerType === "touch" && state.open.peek())
|
|
177
|
+
choose(i);
|
|
178
|
+
}}
|
|
179
|
+
@click=${() => { if (state.open.peek())
|
|
180
|
+
choose(i); }}>${o.label}</div>`)}
|
|
120
181
|
</div>
|
|
121
182
|
</div>`;
|
|
122
183
|
}
|
|
184
|
+
/** A printable character typed with no command modifier. */
|
|
185
|
+
function isTyping(e) {
|
|
186
|
+
return e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
187
|
+
}
|
package/dist/select/select.d.ts
CHANGED
|
@@ -11,6 +11,18 @@ export declare class Select extends BoundField {
|
|
|
11
11
|
#private;
|
|
12
12
|
get choices(): readonly Option[] | undefined;
|
|
13
13
|
set choices(v: readonly Option[] | undefined);
|
|
14
|
+
/** Shown when no value is chosen. Default "Choose…". */
|
|
15
|
+
get placeholder(): string;
|
|
16
|
+
set placeholder(v: string);
|
|
17
|
+
/** Filters the allowed values as the person types. Default `false`. */
|
|
18
|
+
get filter(): boolean;
|
|
19
|
+
set filter(v: boolean);
|
|
20
|
+
/** Shows a clear button while a value is chosen; only acts on a nullable field. Default `false`. */
|
|
21
|
+
get clearable(): boolean;
|
|
22
|
+
set clearable(v: boolean);
|
|
23
|
+
/** What the filtered list says when nothing matches. Default "No matches". */
|
|
24
|
+
get noResults(): string;
|
|
25
|
+
set noResults(v: string);
|
|
14
26
|
focus(options?: FocusOptions): void;
|
|
15
27
|
protected control(c: ControlContext): View;
|
|
16
28
|
}
|
package/dist/select/select.js
CHANGED
|
@@ -10,12 +10,44 @@ import { ChoiceState, choiceView } from "./choice.js";
|
|
|
10
10
|
export class Select extends BoundField {
|
|
11
11
|
#state = new ChoiceState();
|
|
12
12
|
#choices = this.prop(undefined);
|
|
13
|
+
#placeholder = this.prop("Choose…");
|
|
14
|
+
#filter = this.prop(false);
|
|
15
|
+
#clearable = this.prop(false);
|
|
16
|
+
#noResults = this.prop("No matches");
|
|
13
17
|
get choices() {
|
|
14
18
|
return this.#choices.value;
|
|
15
19
|
}
|
|
16
20
|
set choices(v) {
|
|
17
21
|
this.#choices.value = v;
|
|
18
22
|
}
|
|
23
|
+
/** Shown when no value is chosen. Default "Choose…". */
|
|
24
|
+
get placeholder() {
|
|
25
|
+
return this.#placeholder.value;
|
|
26
|
+
}
|
|
27
|
+
set placeholder(v) {
|
|
28
|
+
this.#placeholder.value = v;
|
|
29
|
+
}
|
|
30
|
+
/** Filters the allowed values as the person types. Default `false`. */
|
|
31
|
+
get filter() {
|
|
32
|
+
return this.#filter.value;
|
|
33
|
+
}
|
|
34
|
+
set filter(v) {
|
|
35
|
+
this.#filter.value = v;
|
|
36
|
+
}
|
|
37
|
+
/** Shows a clear button while a value is chosen; only acts on a nullable field. Default `false`. */
|
|
38
|
+
get clearable() {
|
|
39
|
+
return this.#clearable.value;
|
|
40
|
+
}
|
|
41
|
+
set clearable(v) {
|
|
42
|
+
this.#clearable.value = v;
|
|
43
|
+
}
|
|
44
|
+
/** What the filtered list says when nothing matches. Default "No matches". */
|
|
45
|
+
get noResults() {
|
|
46
|
+
return this.#noResults.value;
|
|
47
|
+
}
|
|
48
|
+
set noResults(v) {
|
|
49
|
+
this.#noResults.value = v;
|
|
50
|
+
}
|
|
19
51
|
focus(options) {
|
|
20
52
|
this.querySelector("[role=combobox]")?.focus(options);
|
|
21
53
|
}
|
|
@@ -27,8 +59,12 @@ export class Select extends BoundField {
|
|
|
27
59
|
...(c.meta?.allowedValues ?? []).map((a) => ({ value: a.value, label: a.label ?? String(a.value) })),
|
|
28
60
|
],
|
|
29
61
|
value: c.value,
|
|
30
|
-
placeholder:
|
|
62
|
+
placeholder: this.#placeholder.value,
|
|
31
63
|
readOnly: c.state.readOnly,
|
|
64
|
+
disabled: !c.state.enabled,
|
|
65
|
+
filter: this.#filter.value,
|
|
66
|
+
clearable: this.#clearable.value && c.meta?.nullable === true,
|
|
67
|
+
noResults: this.#noResults.value,
|
|
32
68
|
...(this.labelled ? { labelledBy: `${c.id}-label` } : { label: c.label }),
|
|
33
69
|
required: c.state.required,
|
|
34
70
|
invalid: c.invalid,
|
|
@@ -34,3 +34,17 @@ export declare const TOKEN_NAMES: readonly string[];
|
|
|
34
34
|
export declare const DENSITY: readonly ["ctl", "ctl-tight", "pad-y", "row-y", "pad", "gap"];
|
|
35
35
|
export declare const DENSITIES: readonly ["compact", "standard", "comfortable"];
|
|
36
36
|
export declare const DENSITY_NAMES: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Component geometry intents, by component. Unlike [[TOKENS]] an identity
|
|
39
|
+
* need not define these: each has a default in `base.css`, and a component
|
|
40
|
+
* uses it for geometry CSS owns. An app may set one on a placement.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COMPONENT_VARS: {
|
|
43
|
+
readonly form: readonly ["label-width"];
|
|
44
|
+
readonly grid: readonly ["header-height", "toolbar-height", "row-min-height", "tree-indent", "summary-height"];
|
|
45
|
+
readonly menu: readonly ["cell-height", "icon-size"];
|
|
46
|
+
readonly window: readonly ["header-height"];
|
|
47
|
+
readonly dialog: readonly ["max-width"];
|
|
48
|
+
};
|
|
49
|
+
/** Every name in [[constant:COMPONENT_VARS]] as a full custom property name, such as `--aster-grid-header-height`. */
|
|
50
|
+
export declare const COMPONENT_VAR_NAMES: readonly string[];
|
package/dist/styles/contract.js
CHANGED
|
@@ -37,3 +37,18 @@ export const TOKEN_NAMES = Object.values(TOKENS).flat().map((t) => `--aster-${t}
|
|
|
37
37
|
export const DENSITY = ["ctl", "ctl-tight", "pad-y", "row-y", "pad", "gap"];
|
|
38
38
|
export const DENSITIES = ["compact", "standard", "comfortable"];
|
|
39
39
|
export const DENSITY_NAMES = DENSITY.map((t) => `--aster-${t}`);
|
|
40
|
+
/**
|
|
41
|
+
* Component geometry intents, by component. Unlike [[TOKENS]] an identity
|
|
42
|
+
* need not define these: each has a default in `base.css`, and a component
|
|
43
|
+
* uses it for geometry CSS owns. An app may set one on a placement.
|
|
44
|
+
*/
|
|
45
|
+
export const COMPONENT_VARS = {
|
|
46
|
+
form: ["label-width"],
|
|
47
|
+
grid: ["header-height", "toolbar-height", "row-min-height", "tree-indent", "summary-height"],
|
|
48
|
+
menu: ["cell-height", "icon-size"],
|
|
49
|
+
window: ["header-height"],
|
|
50
|
+
dialog: ["max-width"],
|
|
51
|
+
};
|
|
52
|
+
/** Every name in [[constant:COMPONENT_VARS]] as a full custom property name, such as `--aster-grid-header-height`. */
|
|
53
|
+
export const COMPONENT_VAR_NAMES = Object.entries(COMPONENT_VARS)
|
|
54
|
+
.flatMap(([component, names]) => names.map((name) => `--aster-${component}-${name}`));
|
package/dist/switch/switch.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface SwitchBinding {
|
|
|
11
11
|
* whole row toggles it. A toggle saves straight away: the switch shows the
|
|
12
12
|
* new state while the save runs, and if the save fails it goes back and the
|
|
13
13
|
* error shows under the row. The label is the field's title from metadata.
|
|
14
|
+
*
|
|
15
|
+
* `readOnly` keeps the switch focusable and announced but stops changes;
|
|
16
|
+
* `disabled` takes it out of the tab order as well. `onLabel` and `offLabel`
|
|
17
|
+
* reword the visible state ("Visible" / "Hidden").
|
|
14
18
|
*/
|
|
15
19
|
export declare class Switch extends AsterElement {
|
|
16
20
|
#private;
|
|
@@ -18,5 +22,17 @@ export declare class Switch extends AsterElement {
|
|
|
18
22
|
set binding(v: SwitchBinding | undefined);
|
|
19
23
|
get label(): string;
|
|
20
24
|
set label(v: string);
|
|
25
|
+
/** Removes the switch from the tab order and stops it toggling. Default `false`. */
|
|
26
|
+
get disabled(): boolean;
|
|
27
|
+
set disabled(v: boolean);
|
|
28
|
+
/** Keeps the switch focusable but stops it toggling. Default `false`. */
|
|
29
|
+
get readOnly(): boolean;
|
|
30
|
+
set readOnly(v: boolean);
|
|
31
|
+
/** Text shown while the switch is on. Default "On" when either label is given. */
|
|
32
|
+
get onLabel(): string | undefined;
|
|
33
|
+
set onLabel(v: string | undefined);
|
|
34
|
+
/** Text shown while the switch is off. Default "Off" when either label is given. */
|
|
35
|
+
get offLabel(): string | undefined;
|
|
36
|
+
set offLabel(v: string | undefined);
|
|
21
37
|
protected render(): View;
|
|
22
38
|
}
|
package/dist/switch/switch.js
CHANGED
|
@@ -6,12 +6,20 @@ import { AsterElement, define, uid } from "../base/element.js";
|
|
|
6
6
|
* whole row toggles it. A toggle saves straight away: the switch shows the
|
|
7
7
|
* new state while the save runs, and if the save fails it goes back and the
|
|
8
8
|
* error shows under the row. The label is the field's title from metadata.
|
|
9
|
+
*
|
|
10
|
+
* `readOnly` keeps the switch focusable and announced but stops changes;
|
|
11
|
+
* `disabled` takes it out of the tab order as well. `onLabel` and `offLabel`
|
|
12
|
+
* reword the visible state ("Visible" / "Hidden").
|
|
9
13
|
*/
|
|
10
14
|
export class Switch extends AsterElement {
|
|
11
15
|
#binding = this.prop(undefined);
|
|
12
16
|
#label = this.prop("");
|
|
13
17
|
#pending = this.prop(undefined);
|
|
14
18
|
#error = this.prop(undefined);
|
|
19
|
+
#disabled = this.prop(false);
|
|
20
|
+
#readOnly = this.prop(false);
|
|
21
|
+
#onLabel = this.prop(undefined);
|
|
22
|
+
#offLabel = this.prop(undefined);
|
|
15
23
|
#id = uid("aster-switch");
|
|
16
24
|
get binding() {
|
|
17
25
|
return this.#binding.value;
|
|
@@ -25,6 +33,34 @@ export class Switch extends AsterElement {
|
|
|
25
33
|
set label(v) {
|
|
26
34
|
this.#label.value = v;
|
|
27
35
|
}
|
|
36
|
+
/** Removes the switch from the tab order and stops it toggling. Default `false`. */
|
|
37
|
+
get disabled() {
|
|
38
|
+
return this.#disabled.value;
|
|
39
|
+
}
|
|
40
|
+
set disabled(v) {
|
|
41
|
+
this.#disabled.value = v;
|
|
42
|
+
}
|
|
43
|
+
/** Keeps the switch focusable but stops it toggling. Default `false`. */
|
|
44
|
+
get readOnly() {
|
|
45
|
+
return this.#readOnly.value;
|
|
46
|
+
}
|
|
47
|
+
set readOnly(v) {
|
|
48
|
+
this.#readOnly.value = v;
|
|
49
|
+
}
|
|
50
|
+
/** Text shown while the switch is on. Default "On" when either label is given. */
|
|
51
|
+
get onLabel() {
|
|
52
|
+
return this.#onLabel.value;
|
|
53
|
+
}
|
|
54
|
+
set onLabel(v) {
|
|
55
|
+
this.#onLabel.value = v;
|
|
56
|
+
}
|
|
57
|
+
/** Text shown while the switch is off. Default "Off" when either label is given. */
|
|
58
|
+
get offLabel() {
|
|
59
|
+
return this.#offLabel.value;
|
|
60
|
+
}
|
|
61
|
+
set offLabel(v) {
|
|
62
|
+
this.#offLabel.value = v;
|
|
63
|
+
}
|
|
28
64
|
render() {
|
|
29
65
|
const b = this.#binding.value;
|
|
30
66
|
if (!b)
|
|
@@ -34,23 +70,37 @@ export class Switch extends AsterElement {
|
|
|
34
70
|
const on = pending ?? stored === true;
|
|
35
71
|
const label = this.#label.value || b.ctx.schema.set(b.set).type.field(b.field)?.title || b.field;
|
|
36
72
|
const error = this.#error.value;
|
|
37
|
-
|
|
73
|
+
const labelled = this.#onLabel.value !== undefined || this.#offLabel.value !== undefined;
|
|
74
|
+
const stateText = labelled ? (on ? this.#onLabel.value ?? "On" : this.#offLabel.value ?? "Off") : undefined;
|
|
75
|
+
const stateId = stateText === undefined ? undefined : `${this.#id}-state`;
|
|
76
|
+
const describedBy = [stateId ?? "", error ? `${this.#id}-error` : ""].filter(Boolean).join(" ") || undefined;
|
|
77
|
+
return html `<div class="aster-switch-row" data-click
|
|
78
|
+
?data-disabled=${this.#disabled.value} ?data-readonly=${this.#readOnly.value}
|
|
79
|
+
@click=${(e) => this.#rowClick(e)}>
|
|
38
80
|
<span id=${`${this.#id}-label`}>${label}</span>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
aria-
|
|
42
|
-
|
|
81
|
+
<span class="aster-switch-end">
|
|
82
|
+
${stateText === undefined ? nothing : html `<span class="aster-switch-state" id=${stateId}>${stateText}</span>`}
|
|
83
|
+
<button type="button" class="aster-switch" role="switch" aria-checked=${on ? "true" : "false"}
|
|
84
|
+
aria-labelledby=${`${this.#id}-label`} aria-busy=${pending === undefined ? "false" : "true"}
|
|
85
|
+
aria-disabled=${this.#readOnly.value ? "true" : nothing}
|
|
86
|
+
aria-readonly=${this.#readOnly.value ? "true" : nothing}
|
|
87
|
+
aria-describedby=${describedBy ?? nothing}
|
|
88
|
+
?disabled=${this.#disabled.value}
|
|
89
|
+
@click=${() => this.#toggle(!on)}></button>
|
|
90
|
+
</span>
|
|
43
91
|
</div>
|
|
44
92
|
${error ? html `<p class="aster-error" id=${`${this.#id}-error`} role="alert">${error}</p>` : nothing}`;
|
|
45
93
|
}
|
|
46
94
|
#rowClick(e) {
|
|
95
|
+
if (this.#disabled.peek() || this.#readOnly.peek())
|
|
96
|
+
return;
|
|
47
97
|
if (e.target.closest('[role="switch"]'))
|
|
48
98
|
return;
|
|
49
99
|
this.querySelector('[role="switch"]')?.click();
|
|
50
100
|
}
|
|
51
101
|
async #toggle(to) {
|
|
52
102
|
const b = this.#binding.peek();
|
|
53
|
-
if (!b || this.#pending.peek() !== undefined)
|
|
103
|
+
if (!b || this.#disabled.peek() || this.#readOnly.peek() || this.#pending.peek() !== undefined)
|
|
54
104
|
return;
|
|
55
105
|
this.#pending.value = to;
|
|
56
106
|
this.#error.value = undefined;
|
package/dist/tree/navigator.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export declare class TreeNavigator extends AsterElement {
|
|
|
28
28
|
set choosable(v: ((entity: Entity) => string | undefined) | undefined);
|
|
29
29
|
/** The level shown, as keys from the top. */
|
|
30
30
|
get path(): readonly KeyText[];
|
|
31
|
+
/** Whether the bar carries a search field that narrows the level's loaded rows. Default `false`. */
|
|
32
|
+
get search(): boolean;
|
|
33
|
+
set search(v: boolean);
|
|
31
34
|
/** Shows the level under `key` (`undefined`: the top), as if each level were opened in turn. */
|
|
32
35
|
go(key: KeyText | undefined): Promise<void>;
|
|
33
36
|
focus(options?: FocusOptions): void;
|
package/dist/tree/navigator.js
CHANGED
|
@@ -18,6 +18,8 @@ export class TreeNavigator extends AsterElement {
|
|
|
18
18
|
#chooseLabel = this.prop("Choose");
|
|
19
19
|
#headingLevel = this.prop(2);
|
|
20
20
|
#choosable = this.prop(undefined);
|
|
21
|
+
#search = this.prop(false);
|
|
22
|
+
#query = this.prop("");
|
|
21
23
|
/** Keys from the top level down to the level shown. */
|
|
22
24
|
#path = signal([]);
|
|
23
25
|
#back = signal([]);
|
|
@@ -59,6 +61,15 @@ export class TreeNavigator extends AsterElement {
|
|
|
59
61
|
get path() {
|
|
60
62
|
return this.#path.value;
|
|
61
63
|
}
|
|
64
|
+
/** Whether the bar carries a search field that narrows the level's loaded rows. Default `false`. */
|
|
65
|
+
get search() {
|
|
66
|
+
return this.#search.value;
|
|
67
|
+
}
|
|
68
|
+
set search(v) {
|
|
69
|
+
this.#search.value = v;
|
|
70
|
+
if (!v)
|
|
71
|
+
this.#query.value = "";
|
|
72
|
+
}
|
|
62
73
|
/** Shows the level under `key` (`undefined`: the top), as if each level were opened in turn. */
|
|
63
74
|
async go(key) {
|
|
64
75
|
const tree = this.#tree.peek();
|
|
@@ -83,6 +94,8 @@ export class TreeNavigator extends AsterElement {
|
|
|
83
94
|
return e ? String(e[display] ?? "") : "…";
|
|
84
95
|
};
|
|
85
96
|
const loaded = level.keys.value.filter((k) => k !== undefined);
|
|
97
|
+
const query = this.#query.value.trim().toLowerCase();
|
|
98
|
+
const matches = query ? loaded.filter((key) => title(key).toLowerCase().includes(query)) : loaded;
|
|
86
99
|
const count = level.count.value;
|
|
87
100
|
if (count === undefined && level.status.peek() !== "loading")
|
|
88
101
|
queueMicrotask(() => void level.ensure(0, level.pageSize).catch(() => { }));
|
|
@@ -99,6 +112,10 @@ export class TreeNavigator extends AsterElement {
|
|
|
99
112
|
@click=${() => this.#move(path.slice(0, i + 1), i + 1 >= path.length ? 1 : -1)}>${title(k)}</button></li>`)}
|
|
100
113
|
</ol>
|
|
101
114
|
</nav>
|
|
115
|
+
${this.#search.value ? html `<div class="aster-drill-search">
|
|
116
|
+
<input type="search" placeholder="Search" aria-label=${`Search ${this.#label.value || "this level"}`}
|
|
117
|
+
.value=${this.#query.value} @input=${(e) => { this.#query.value = e.target.value; }}>
|
|
118
|
+
</div>` : nothing}
|
|
102
119
|
</div>
|
|
103
120
|
<div class="aster-drill-heading" role="heading" aria-level=${this.#headingLevel.value} id=${`${this.#id}-heading`} tabindex="-1">${heading}</div>
|
|
104
121
|
<div class="aster-drill-viewport">
|
|
@@ -110,7 +127,8 @@ export class TreeNavigator extends AsterElement {
|
|
|
110
127
|
}}>
|
|
111
128
|
${count === undefined ? html `<li class="aster-drill-empty"><span role="status">Loading…</span></li>` : nothing}
|
|
112
129
|
${count === 0 ? html `<li class="aster-drill-empty">Nothing here.</li>` : nothing}
|
|
113
|
-
${loaded.
|
|
130
|
+
${query && loaded.length > 0 && matches.length === 0 ? html `<li class="aster-drill-empty"><span role="status">Nothing matches.</span></li>` : nothing}
|
|
131
|
+
${matches.map((key) => {
|
|
114
132
|
const entity = this.#entity(key);
|
|
115
133
|
const name = title(key);
|
|
116
134
|
const folder = tree.hasChildren(key, parent);
|
|
@@ -128,7 +146,7 @@ export class TreeNavigator extends AsterElement {
|
|
|
128
146
|
: nothing}
|
|
129
147
|
</li>`;
|
|
130
148
|
})}
|
|
131
|
-
${count !== undefined && loaded.length < count ? html `<li class="aster-drill-empty"><span role="status">${loaded.length} of ${count}…</span></li>` : nothing}
|
|
149
|
+
${!query && count !== undefined && loaded.length < count ? html `<li class="aster-drill-empty"><span role="status">${loaded.length} of ${count}…</span></li>` : nothing}
|
|
132
150
|
</ul>
|
|
133
151
|
</div>
|
|
134
152
|
</section>`;
|
|
@@ -151,6 +169,7 @@ export class TreeNavigator extends AsterElement {
|
|
|
151
169
|
return this.#tree.value?.entity(key);
|
|
152
170
|
}
|
|
153
171
|
#move(path, direction, replaceHistory = false) {
|
|
172
|
+
this.#query.value = "";
|
|
154
173
|
const from = this.#path.peek();
|
|
155
174
|
if (from.join() === path.join())
|
|
156
175
|
return;
|
|
@@ -216,6 +235,11 @@ export class TreeNavigator extends AsterElement {
|
|
|
216
235
|
#key(e) {
|
|
217
236
|
if (e.key === "Escape") {
|
|
218
237
|
e.stopPropagation();
|
|
238
|
+
if (e.target instanceof HTMLInputElement && this.#query.peek()) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
this.#query.value = "";
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
219
243
|
this.emit("cancel");
|
|
220
244
|
}
|
|
221
245
|
else if (e.key === "Backspace" && !e.target.matches("input, textarea") && this.#path.peek().length) {
|
package/dist/window/chrome.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { View } from "../base/element.ts";
|
|
2
|
+
export interface WindowBarOptions {
|
|
3
|
+
/** The heading's outline level. `2` is the native h2 level and adds no attribute. */
|
|
4
|
+
readonly level?: number;
|
|
5
|
+
}
|
|
2
6
|
/** Shared title/chrome geometry for floating windows and dashboard portlets. */
|
|
3
|
-
export declare function windowBar(id: string, heading: string, actions: View, drag?: (event: PointerEvent) => void): View;
|
|
7
|
+
export declare function windowBar(id: string, heading: string | View, actions: View, drag?: (event: PointerEvent) => void, options?: WindowBarOptions): View;
|
package/dist/window/chrome.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { html } from "lit-html";
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
2
|
/** Shared title/chrome geometry for floating windows and dashboard portlets. */
|
|
3
|
-
export function windowBar(id, heading, actions, drag) {
|
|
3
|
+
export function windowBar(id, heading, actions, drag, options) {
|
|
4
|
+
const level = options?.level === undefined || options.level === 2 ? nothing : options.level;
|
|
4
5
|
return html `<div class="aster-window-bar" @pointerdown=${drag}>
|
|
5
|
-
<h2 id=${id}>${heading}</h2>${actions}
|
|
6
|
+
<h2 id=${id} aria-level=${level}>${heading}</h2>${actions}
|
|
6
7
|
</div>`;
|
|
7
8
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The parts of a floating window's chrome it draws. Omitted fields keep their defaults. */
|
|
2
|
+
export interface WindowChrome {
|
|
3
|
+
/** The title bar: heading, move control and window controls. Default `true`. */
|
|
4
|
+
readonly header?: boolean;
|
|
5
|
+
/** The heading inside the title bar. Default `true`; a hidden heading still names the window. */
|
|
6
|
+
readonly title?: boolean;
|
|
7
|
+
/** The Close control. Default `true`. */
|
|
8
|
+
readonly close?: boolean;
|
|
9
|
+
/** The Minimize control. Default: shown for managed windows. */
|
|
10
|
+
readonly minimize?: boolean;
|
|
11
|
+
/** The Maximize/Restore control. Default: shown for managed windows. */
|
|
12
|
+
readonly maximize?: boolean;
|
|
13
|
+
/** Pointer and keyboard resizing. Default `true`. */
|
|
14
|
+
readonly resizer?: boolean;
|
|
15
|
+
/** The footer region at the bottom of the window. Default `false`; setting `footer` turns it on. */
|
|
16
|
+
readonly footer?: boolean;
|
|
17
|
+
/** The status region at the bottom of the window. Default `false`; setting `status` turns it on. */
|
|
18
|
+
readonly statusBar?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type WindowState = "normal" | "maximized" | "minimized";
|
|
21
|
+
export interface WindowBounds {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
export interface WindowPlacement {
|
|
28
|
+
state: WindowState;
|
|
29
|
+
bounds?: WindowBounds;
|
|
30
|
+
restoreMaximized?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** [[object:WindowChrome]] with every default filled in. */
|
|
33
|
+
export interface ResolvedWindowChrome {
|
|
34
|
+
readonly header: boolean;
|
|
35
|
+
readonly title: boolean;
|
|
36
|
+
readonly close: boolean;
|
|
37
|
+
readonly minimize: boolean;
|
|
38
|
+
readonly maximize: boolean;
|
|
39
|
+
readonly resizer: boolean;
|
|
40
|
+
readonly footer: boolean;
|
|
41
|
+
readonly statusBar: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Minimize and maximize stay managed-window chrome unless the host overrides them. */
|
|
44
|
+
export declare function resolveWindowChrome(chrome: WindowChrome, managed: boolean): ResolvedWindowChrome;
|
|
45
|
+
/** A placement is rejected atomically; bounds are finite and positive. */
|
|
46
|
+
export declare function parseWindowPlacement(value: unknown): WindowPlacement;
|
|
47
|
+
/** The rectangle a window may occupy, in viewport pixels. */
|
|
48
|
+
export interface WindowLimits {
|
|
49
|
+
left: number;
|
|
50
|
+
top: number;
|
|
51
|
+
right: number;
|
|
52
|
+
bottom: number;
|
|
53
|
+
}
|
|
54
|
+
/** Keeps a window's top-left inside `limits`, never computing layout of its own. */
|
|
55
|
+
export declare function clampWindow(at: {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
}, size: {
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
}, limits: WindowLimits): {
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
};
|
|
65
|
+
export declare function centerWindow(size: {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
}, limits: WindowLimits): {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
};
|