@getaster/elements 0.0.0-stage → 0.1.0-preview.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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +6 -2
- package/bin/aster-theme-check.js +317 -0
- package/dist/base/drag.d.ts +45 -0
- package/dist/base/drag.js +139 -0
- package/dist/base/element.d.ts +51 -0
- package/dist/base/element.js +133 -0
- package/dist/breadcrumbs/breadcrumbs.d.ts +23 -0
- package/dist/breadcrumbs/breadcrumbs.js +51 -0
- package/dist/button/button.d.ts +37 -0
- package/dist/button/button.js +108 -0
- package/dist/caret/caret-group.d.ts +14 -0
- package/dist/caret/caret-group.js +71 -0
- package/dist/carousel/carousel.d.ts +28 -0
- package/dist/carousel/carousel.js +134 -0
- package/dist/component/component.d.ts +73 -0
- package/dist/component/component.js +190 -0
- package/dist/component/echarts.d.ts +46 -0
- package/dist/component/echarts.js +95 -0
- package/dist/composition/authoring.d.ts +157 -0
- package/dist/composition/authoring.js +291 -0
- package/dist/composition/builtins.d.ts +1130 -0
- package/dist/composition/builtins.js +318 -0
- package/dist/composition/command-bar.d.ts +18 -0
- package/dist/composition/command-bar.js +34 -0
- package/dist/composition/commands.d.ts +14 -0
- package/dist/composition/commands.js +59 -0
- package/dist/composition/composite.d.ts +48 -0
- package/dist/composition/composite.js +409 -0
- package/dist/composition/contracts.d.ts +63 -0
- package/dist/composition/contracts.js +97 -0
- package/dist/composition/definition.d.ts +94 -0
- package/dist/composition/definition.js +291 -0
- package/dist/composition/element.d.ts +35 -0
- package/dist/composition/element.js +51 -0
- package/dist/composition/registry.d.ts +83 -0
- package/dist/composition/registry.js +99 -0
- package/dist/composition/schema.d.ts +9 -0
- package/dist/composition/schema.js +139 -0
- package/dist/composition/sessions.d.ts +15 -0
- package/dist/composition/sessions.js +61 -0
- package/dist/dashboard/dashboard.d.ts +26 -0
- package/dist/dashboard/dashboard.js +296 -0
- package/dist/dashboard/layout.d.ts +19 -0
- package/dist/dashboard/layout.js +48 -0
- package/dist/dashboard/widget.d.ts +42 -0
- package/dist/dashboard/widget.js +165 -0
- package/dist/date/calendar.d.ts +34 -0
- package/dist/date/calendar.js +137 -0
- package/dist/date/date-field.d.ts +18 -0
- package/dist/date/date-field.js +81 -0
- package/dist/date/dates.d.ts +22 -0
- package/dist/date/dates.js +80 -0
- package/dist/dialog/dialog.d.ts +23 -0
- package/dist/dialog/dialog.js +70 -0
- package/dist/field/bound-field.d.ts +55 -0
- package/dist/field/bound-field.js +139 -0
- package/dist/field/check.d.ts +11 -0
- package/dist/field/check.js +26 -0
- package/dist/field/composite-field.d.ts +35 -0
- package/dist/field/composite-field.js +88 -0
- package/dist/field/field.d.ts +29 -0
- package/dist/field/field.js +75 -0
- package/dist/field/input.d.ts +15 -0
- package/dist/field/input.js +18 -0
- package/dist/field/number-field.d.ts +33 -0
- package/dist/field/number-field.js +179 -0
- package/dist/field/numeric.d.ts +4 -0
- package/dist/field/numeric.js +57 -0
- package/dist/field/values.d.ts +14 -0
- package/dist/field/values.js +60 -0
- package/dist/filter/filter-builder.d.ts +18 -0
- package/dist/filter/filter-builder.js +305 -0
- package/dist/form/controls.d.ts +20 -0
- package/dist/form/controls.js +34 -0
- package/dist/form/form.d.ts +46 -0
- package/dist/form/form.js +85 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +57 -0
- package/dist/grid/filters.d.ts +16 -0
- package/dist/grid/filters.js +66 -0
- package/dist/grid/grid.d.ts +209 -0
- package/dist/grid/grid.js +2587 -0
- package/dist/grid/rows.d.ts +32 -0
- package/dist/grid/rows.js +89 -0
- package/dist/grid/view.d.ts +69 -0
- package/dist/grid/view.js +172 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +61 -0
- package/dist/layout/stack.d.ts +112 -0
- package/dist/layout/stack.js +21 -0
- package/dist/list-edit/list-edit.d.ts +42 -0
- package/dist/list-edit/list-edit.js +220 -0
- package/dist/listbox/listbox.d.ts +45 -0
- package/dist/listbox/listbox.js +187 -0
- package/dist/menu/menu.d.ts +55 -0
- package/dist/menu/menu.js +286 -0
- package/dist/motion.d.ts +8 -0
- package/dist/motion.js +23 -0
- package/dist/overlay/anchor.d.ts +18 -0
- package/dist/overlay/anchor.js +41 -0
- package/dist/picker/options.d.ts +59 -0
- package/dist/picker/options.js +97 -0
- package/dist/picker/picker.d.ts +46 -0
- package/dist/picker/picker.js +492 -0
- package/dist/picker/tree-choices.d.ts +38 -0
- package/dist/picker/tree-choices.js +72 -0
- package/dist/picker/tree-picker.d.ts +29 -0
- package/dist/picker/tree-picker.js +282 -0
- package/dist/progress/policy.d.ts +5 -0
- package/dist/progress/policy.js +28 -0
- package/dist/progress/status.d.ts +20 -0
- package/dist/progress/status.js +38 -0
- package/dist/rule/operators.d.ts +8 -0
- package/dist/rule/operators.js +9 -0
- package/dist/rule/rule-editor.d.ts +57 -0
- package/dist/rule/rule-editor.js +290 -0
- package/dist/section/accordion.d.ts +24 -0
- package/dist/section/accordion.js +90 -0
- package/dist/section/section.d.ts +30 -0
- package/dist/section/section.js +99 -0
- package/dist/select/choice-element.d.ts +20 -0
- package/dist/select/choice-element.js +55 -0
- package/dist/select/choice.d.ts +34 -0
- package/dist/select/choice.js +122 -0
- package/dist/select/many.d.ts +13 -0
- package/dist/select/many.js +20 -0
- package/dist/select/select.d.ts +16 -0
- package/dist/select/select.js +40 -0
- package/dist/sidebar/sidebar.d.ts +47 -0
- package/dist/sidebar/sidebar.js +105 -0
- package/dist/slicer/slicer.d.ts +26 -0
- package/dist/slicer/slicer.js +140 -0
- package/dist/split/split.d.ts +41 -0
- package/dist/split/split.js +237 -0
- package/dist/styles/contract.d.ts +36 -0
- package/dist/styles/contract.js +39 -0
- package/dist/switch/switch.d.ts +22 -0
- package/dist/switch/switch.js +69 -0
- package/dist/tabs/tabs.d.ts +30 -0
- package/dist/tabs/tabs.js +202 -0
- package/dist/tile-grid/scroll.d.ts +9 -0
- package/dist/tile-grid/scroll.js +21 -0
- package/dist/tile-grid/tile-grid.d.ts +64 -0
- package/dist/tile-grid/tile-grid.js +734 -0
- package/dist/toast/toast.d.ts +25 -0
- package/dist/toast/toast.js +92 -0
- package/dist/tree/navigator.d.ts +41 -0
- package/dist/tree/navigator.js +228 -0
- package/dist/window/chrome.d.ts +3 -0
- package/dist/window/chrome.js +7 -0
- package/dist/window/window.d.ts +46 -0
- package/dist/window/window.js +347 -0
- package/dist/window/workspace.d.ts +29 -0
- package/dist/window/workspace.js +137 -0
- package/package.json +45 -4
- package/styles/aster-ui.css +1147 -0
- package/styles/base.css +88 -0
- package/styles/contract.ts +41 -0
- package/styles/elements.css +1058 -0
- package/styles/harbour.css +126 -0
- package/styles/lamplight.css +118 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
+
export interface ToastAction {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly run: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Toasts, one at a time, for results with no visible place of their own or
|
|
8
|
+
* to offer Undo. A toast stays `duration` milliseconds, or `actionDuration`
|
|
9
|
+
* with an action. Hovering over it or focusing inside it holds it; once
|
|
10
|
+
* neither applies it stays a further 2 seconds.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Toaster extends AsterElement {
|
|
13
|
+
#private;
|
|
14
|
+
/** How long a toast with no action stays, in milliseconds. Default 1800. */
|
|
15
|
+
get duration(): number;
|
|
16
|
+
set duration(v: number);
|
|
17
|
+
/** How long a toast with an action stays, in milliseconds. Default 6000. */
|
|
18
|
+
get actionDuration(): number;
|
|
19
|
+
set actionDuration(v: number);
|
|
20
|
+
show(message: string, action?: ToastAction): void;
|
|
21
|
+
dismiss(): void;
|
|
22
|
+
protected render(): View;
|
|
23
|
+
}
|
|
24
|
+
/** Tells screen readers about a background result, with no popup. */
|
|
25
|
+
export declare function announce(text: string): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
|
+
import { AsterElement, define } from "../base/element.js";
|
|
3
|
+
/**
|
|
4
|
+
* Toasts, one at a time, for results with no visible place of their own or
|
|
5
|
+
* to offer Undo. A toast stays `duration` milliseconds, or `actionDuration`
|
|
6
|
+
* with an action. Hovering over it or focusing inside it holds it; once
|
|
7
|
+
* neither applies it stays a further 2 seconds.
|
|
8
|
+
*/
|
|
9
|
+
export class Toaster extends AsterElement {
|
|
10
|
+
#shown = this.prop(undefined);
|
|
11
|
+
#visible = this.prop(false);
|
|
12
|
+
#duration = this.prop(1800);
|
|
13
|
+
#actionDuration = this.prop(6000);
|
|
14
|
+
#hovered = false;
|
|
15
|
+
#focused = false;
|
|
16
|
+
#timer;
|
|
17
|
+
#ids = 0;
|
|
18
|
+
/** How long a toast with no action stays, in milliseconds. Default 1800. */
|
|
19
|
+
get duration() {
|
|
20
|
+
return this.#duration.value;
|
|
21
|
+
}
|
|
22
|
+
set duration(v) {
|
|
23
|
+
this.#duration.value = v;
|
|
24
|
+
}
|
|
25
|
+
/** How long a toast with an action stays, in milliseconds. Default 6000. */
|
|
26
|
+
get actionDuration() {
|
|
27
|
+
return this.#actionDuration.value;
|
|
28
|
+
}
|
|
29
|
+
set actionDuration(v) {
|
|
30
|
+
this.#actionDuration.value = v;
|
|
31
|
+
}
|
|
32
|
+
show(message, action) {
|
|
33
|
+
this.#shown.value = { message, id: ++this.#ids, ...(action ? { action } : {}) };
|
|
34
|
+
this.#visible.value = true;
|
|
35
|
+
// The old toast's button may still hold focus; the new one keeps waiting for it to leave.
|
|
36
|
+
this.#focused = this.contains(document.activeElement);
|
|
37
|
+
if (!this.#focused)
|
|
38
|
+
this.#arm(action ? this.#actionDuration.peek() : this.#duration.peek());
|
|
39
|
+
}
|
|
40
|
+
dismiss() {
|
|
41
|
+
clearTimeout(this.#timer);
|
|
42
|
+
this.#hovered = this.#focused = false;
|
|
43
|
+
this.#visible.value = false;
|
|
44
|
+
}
|
|
45
|
+
render() {
|
|
46
|
+
const shown = this.#shown.value;
|
|
47
|
+
return html `<div class="aster-toast" role="status" ?data-show=${this.#visible.value}
|
|
48
|
+
@pointerenter=${() => this.#hold(true, undefined)}
|
|
49
|
+
@pointerleave=${() => this.#hold(false, undefined)}
|
|
50
|
+
@focusin=${() => this.#hold(undefined, true)}
|
|
51
|
+
@focusout=${(e) => !this.contains(e.relatedTarget) && this.#hold(undefined, false)}>
|
|
52
|
+
<i aria-hidden="true"></i><span>${shown?.message ?? ""}</span>
|
|
53
|
+
${shown?.action && this.#visible.value
|
|
54
|
+
? html `<button type="button" @click=${() => this.#act(shown.action)}>${shown.action.label}</button>`
|
|
55
|
+
: nothing}
|
|
56
|
+
</div>`;
|
|
57
|
+
}
|
|
58
|
+
#act(action) {
|
|
59
|
+
this.dismiss();
|
|
60
|
+
action.run();
|
|
61
|
+
}
|
|
62
|
+
/** Pointer and focus each hold the toast; the timer restarts once neither does. */
|
|
63
|
+
#hold(hovered, focused) {
|
|
64
|
+
this.#hovered = hovered ?? this.#hovered;
|
|
65
|
+
this.#focused = focused ?? this.#focused;
|
|
66
|
+
if (this.#hovered || this.#focused)
|
|
67
|
+
clearTimeout(this.#timer);
|
|
68
|
+
else if (this.#visible.peek())
|
|
69
|
+
this.#arm(2000);
|
|
70
|
+
}
|
|
71
|
+
#arm(ms) {
|
|
72
|
+
clearTimeout(this.#timer);
|
|
73
|
+
this.#timer = setTimeout(() => {
|
|
74
|
+
this.#hovered = this.#focused = false;
|
|
75
|
+
this.#visible.value = false;
|
|
76
|
+
}, ms);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
define("aster-toaster", Toaster);
|
|
80
|
+
let region;
|
|
81
|
+
/** Tells screen readers about a background result, with no popup. */
|
|
82
|
+
export function announce(text) {
|
|
83
|
+
if (!region?.isConnected) {
|
|
84
|
+
region = document.createElement("p");
|
|
85
|
+
region.className = "aster-sr-only";
|
|
86
|
+
region.setAttribute("aria-live", "polite");
|
|
87
|
+
document.body.append(region);
|
|
88
|
+
}
|
|
89
|
+
region.textContent = "";
|
|
90
|
+
const r = region;
|
|
91
|
+
requestAnimationFrame(() => (r.textContent = text));
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Entity, KeyText, Tree } from "@getaster/data";
|
|
2
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A tree, one level at a time: each row chooses its node, or opens it to
|
|
5
|
+
* show the level under it, sliding in (and back out on the way up). A
|
|
6
|
+
* breadcrumb, back and forward move between levels; the level loads by page
|
|
7
|
+
* as it scrolls. Choosing emits `choose` with the node's key; Escape emits
|
|
8
|
+
* `cancel`.
|
|
9
|
+
*
|
|
10
|
+
* The slide clones the outgoing list as an inert ghost and animates both
|
|
11
|
+
* lists; under reduced motion there's no animation.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TreeNavigator extends AsterElement {
|
|
14
|
+
#private;
|
|
15
|
+
get tree(): Tree | undefined;
|
|
16
|
+
set tree(v: Tree | undefined);
|
|
17
|
+
/** The field an entry shows as its name; the type's title field by default. */
|
|
18
|
+
get display(): string;
|
|
19
|
+
set display(v: string);
|
|
20
|
+
/** Names the navigator, and its top level in the breadcrumb. */
|
|
21
|
+
get label(): string;
|
|
22
|
+
set label(v: string);
|
|
23
|
+
/**
|
|
24
|
+
* Says why a node can't be chosen (it can still be opened), or
|
|
25
|
+
* `undefined` if it can: moving a task into its own subtree, say.
|
|
26
|
+
*/
|
|
27
|
+
get choosable(): ((entity: Entity) => string | undefined) | undefined;
|
|
28
|
+
set choosable(v: ((entity: Entity) => string | undefined) | undefined);
|
|
29
|
+
/** The level shown, as keys from the top. */
|
|
30
|
+
get path(): readonly KeyText[];
|
|
31
|
+
/** Shows the level under `key` (`undefined`: the top), as if each level were opened in turn. */
|
|
32
|
+
go(key: KeyText | undefined): Promise<void>;
|
|
33
|
+
focus(options?: FocusOptions): void;
|
|
34
|
+
protected render(): View;
|
|
35
|
+
/** The level of the heading that names the level shown, to fit the page's outline. */
|
|
36
|
+
get headingLevel(): number;
|
|
37
|
+
set headingLevel(v: number);
|
|
38
|
+
/** Text shown on each row's choose button, before the name ("Move under"). */
|
|
39
|
+
get chooseLabel(): string;
|
|
40
|
+
set chooseLabel(v: string);
|
|
41
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { signal } from "@preact/signals-core";
|
|
2
|
+
import { html, nothing, svg } from "lit-html";
|
|
3
|
+
import { AsterElement, define, uid } from "../base/element.js";
|
|
4
|
+
/**
|
|
5
|
+
* A tree, one level at a time: each row chooses its node, or opens it to
|
|
6
|
+
* show the level under it, sliding in (and back out on the way up). A
|
|
7
|
+
* breadcrumb, back and forward move between levels; the level loads by page
|
|
8
|
+
* as it scrolls. Choosing emits `choose` with the node's key; Escape emits
|
|
9
|
+
* `cancel`.
|
|
10
|
+
*
|
|
11
|
+
* The slide clones the outgoing list as an inert ghost and animates both
|
|
12
|
+
* lists; under reduced motion there's no animation.
|
|
13
|
+
*/
|
|
14
|
+
export class TreeNavigator extends AsterElement {
|
|
15
|
+
#tree = this.prop(undefined);
|
|
16
|
+
#display = this.prop("");
|
|
17
|
+
#label = this.prop("");
|
|
18
|
+
#chooseLabel = this.prop("Choose");
|
|
19
|
+
#headingLevel = this.prop(2);
|
|
20
|
+
#choosable = this.prop(undefined);
|
|
21
|
+
/** Keys from the top level down to the level shown. */
|
|
22
|
+
#path = signal([]);
|
|
23
|
+
#back = signal([]);
|
|
24
|
+
#forward = signal([]);
|
|
25
|
+
#id = uid("aster-drill");
|
|
26
|
+
#direction = 1;
|
|
27
|
+
#ghost;
|
|
28
|
+
get tree() {
|
|
29
|
+
return this.#tree.value;
|
|
30
|
+
}
|
|
31
|
+
set tree(v) {
|
|
32
|
+
this.#tree.value = v;
|
|
33
|
+
}
|
|
34
|
+
/** The field an entry shows as its name; the type's title field by default. */
|
|
35
|
+
get display() {
|
|
36
|
+
return this.#display.value;
|
|
37
|
+
}
|
|
38
|
+
set display(v) {
|
|
39
|
+
this.#display.value = v;
|
|
40
|
+
}
|
|
41
|
+
/** Names the navigator, and its top level in the breadcrumb. */
|
|
42
|
+
get label() {
|
|
43
|
+
return this.#label.value;
|
|
44
|
+
}
|
|
45
|
+
set label(v) {
|
|
46
|
+
this.#label.value = v;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Says why a node can't be chosen (it can still be opened), or
|
|
50
|
+
* `undefined` if it can: moving a task into its own subtree, say.
|
|
51
|
+
*/
|
|
52
|
+
get choosable() {
|
|
53
|
+
return this.#choosable.value;
|
|
54
|
+
}
|
|
55
|
+
set choosable(v) {
|
|
56
|
+
this.#choosable.value = v;
|
|
57
|
+
}
|
|
58
|
+
/** The level shown, as keys from the top. */
|
|
59
|
+
get path() {
|
|
60
|
+
return this.#path.value;
|
|
61
|
+
}
|
|
62
|
+
/** Shows the level under `key` (`undefined`: the top), as if each level were opened in turn. */
|
|
63
|
+
async go(key) {
|
|
64
|
+
const tree = this.#tree.peek();
|
|
65
|
+
if (!tree)
|
|
66
|
+
return;
|
|
67
|
+
const path = key === undefined ? [] : [...(await tree.ancestors(key)), key];
|
|
68
|
+
this.#move(path, path.length >= this.#path.peek().length ? 1 : -1, true);
|
|
69
|
+
}
|
|
70
|
+
focus(options) {
|
|
71
|
+
this.querySelector(".aster-drill-heading")?.focus(options);
|
|
72
|
+
}
|
|
73
|
+
render() {
|
|
74
|
+
const tree = this.#tree.value;
|
|
75
|
+
if (!tree)
|
|
76
|
+
return nothing;
|
|
77
|
+
const path = this.#path.value;
|
|
78
|
+
const parent = path.at(-1);
|
|
79
|
+
const level = tree.level(parent);
|
|
80
|
+
const display = this.#display.value || tree.type.titleField || tree.key;
|
|
81
|
+
const title = (key) => {
|
|
82
|
+
const e = this.#entity(key);
|
|
83
|
+
return e ? String(e[display] ?? "") : "…";
|
|
84
|
+
};
|
|
85
|
+
const loaded = level.keys.value.filter((k) => k !== undefined);
|
|
86
|
+
const count = level.count.value;
|
|
87
|
+
if (count === undefined && level.status.peek() !== "loading")
|
|
88
|
+
queueMicrotask(() => void level.ensure(0, level.pageSize).catch(() => { }));
|
|
89
|
+
const heading = parent === undefined ? this.#label.value || "Top level" : title(parent);
|
|
90
|
+
const arrow = (back) => html `<svg viewBox="0 0 16 16" aria-hidden="true"><path d=${back ? "m10 3-5 5 5 5" : "m6 3 5 5-5 5"}/></svg>`;
|
|
91
|
+
return html `<section class="aster-drill" aria-label=${this.#label.value || "Navigate"} @keydown=${(e) => this.#key(e)}>
|
|
92
|
+
<div class="aster-drill-bar">
|
|
93
|
+
<button type="button" class="aster-drill-step" aria-label="Back" title="Back" ?disabled=${!this.#back.value.length} @click=${() => this.#travel(true)}>${arrow(true)}</button>
|
|
94
|
+
<button type="button" class="aster-drill-step" aria-label="Forward" title="Forward" ?disabled=${!this.#forward.value.length} @click=${() => this.#travel(false)}>${arrow(false)}</button>
|
|
95
|
+
<nav class="aster-drill-crumbs" aria-label="Path">
|
|
96
|
+
<ol>
|
|
97
|
+
<li><button type="button" aria-current=${path.length === 0 ? "location" : nothing} @click=${() => this.#move([], -1)}>${this.#label.value || "Top level"}</button></li>
|
|
98
|
+
${path.map((k, i) => html `<li><button type="button" aria-current=${i === path.length - 1 ? "location" : nothing}
|
|
99
|
+
@click=${() => this.#move(path.slice(0, i + 1), i + 1 >= path.length ? 1 : -1)}>${title(k)}</button></li>`)}
|
|
100
|
+
</ol>
|
|
101
|
+
</nav>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="aster-drill-heading" role="heading" aria-level=${this.#headingLevel.value} id=${`${this.#id}-heading`} tabindex="-1">${heading}</div>
|
|
104
|
+
<div class="aster-drill-viewport">
|
|
105
|
+
<ul class="aster-drill-list" aria-labelledby=${`${this.#id}-heading`} aria-busy=${count === undefined ? "true" : "false"}
|
|
106
|
+
@scroll=${(e) => {
|
|
107
|
+
const el = e.currentTarget;
|
|
108
|
+
if (!level.complete.peek() && el.scrollTop + el.clientHeight > el.scrollHeight - 80)
|
|
109
|
+
void level.ensure(0, loaded.length + level.pageSize);
|
|
110
|
+
}}>
|
|
111
|
+
${count === undefined ? html `<li class="aster-drill-empty"><span role="status">Loading…</span></li>` : nothing}
|
|
112
|
+
${count === 0 ? html `<li class="aster-drill-empty">Nothing here.</li>` : nothing}
|
|
113
|
+
${loaded.map((key) => {
|
|
114
|
+
const entity = this.#entity(key);
|
|
115
|
+
const name = title(key);
|
|
116
|
+
const folder = tree.hasChildren(key, parent);
|
|
117
|
+
const why = entity && this.#choosable.value ? this.#choosable.value(entity) : undefined;
|
|
118
|
+
return html `<li class="aster-drill-row" data-key=${key}>
|
|
119
|
+
<button type="button" class="aster-drill-choose" aria-disabled=${why ? "true" : nothing} title=${why ?? nothing}
|
|
120
|
+
aria-label=${`${this.#chooseLabel.value} ${name}`}
|
|
121
|
+
@click=${() => !why && this.emit("choose", key)}>
|
|
122
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">${folder ? svg `<path d="M2 4.5h4.5l1.5 1.5h6v6.5H2z"/>` : svg `<path d="M4 2.5h5.5l3 3v8H4zM9.5 2.5v3h3"/>`}</svg>
|
|
123
|
+
<span>${name}</span>
|
|
124
|
+
</button>
|
|
125
|
+
${folder !== false
|
|
126
|
+
? html `<button type="button" class="aster-drill-open" aria-label=${`Open ${name}`} title=${`Open ${name}`}
|
|
127
|
+
@click=${() => this.#move([...path, key], 1)}>${arrow(false)}</button>`
|
|
128
|
+
: nothing}
|
|
129
|
+
</li>`;
|
|
130
|
+
})}
|
|
131
|
+
${count !== undefined && loaded.length < count ? html `<li class="aster-drill-empty"><span role="status">${loaded.length} of ${count}…</span></li>` : nothing}
|
|
132
|
+
</ul>
|
|
133
|
+
</div>
|
|
134
|
+
</section>`;
|
|
135
|
+
}
|
|
136
|
+
/** The level of the heading that names the level shown, to fit the page's outline. */
|
|
137
|
+
get headingLevel() {
|
|
138
|
+
return this.#headingLevel.value;
|
|
139
|
+
}
|
|
140
|
+
set headingLevel(v) {
|
|
141
|
+
this.#headingLevel.value = v;
|
|
142
|
+
}
|
|
143
|
+
/** Text shown on each row's choose button, before the name ("Move under"). */
|
|
144
|
+
get chooseLabel() {
|
|
145
|
+
return this.#chooseLabel.value;
|
|
146
|
+
}
|
|
147
|
+
set chooseLabel(v) {
|
|
148
|
+
this.#chooseLabel.value = v;
|
|
149
|
+
}
|
|
150
|
+
#entity(key) {
|
|
151
|
+
return this.#tree.value?.entity(key);
|
|
152
|
+
}
|
|
153
|
+
#move(path, direction, replaceHistory = false) {
|
|
154
|
+
const from = this.#path.peek();
|
|
155
|
+
if (from.join() === path.join())
|
|
156
|
+
return;
|
|
157
|
+
if (!replaceHistory) {
|
|
158
|
+
this.#back.value = [...this.#back.peek(), from];
|
|
159
|
+
this.#forward.value = [];
|
|
160
|
+
}
|
|
161
|
+
this.#slide(direction, () => (this.#path.value = path));
|
|
162
|
+
}
|
|
163
|
+
#travel(back) {
|
|
164
|
+
const stack = back ? this.#back.peek() : this.#forward.peek();
|
|
165
|
+
const to = stack.at(-1);
|
|
166
|
+
if (!to)
|
|
167
|
+
return;
|
|
168
|
+
const here = this.#path.peek();
|
|
169
|
+
if (back) {
|
|
170
|
+
this.#back.value = stack.slice(0, -1);
|
|
171
|
+
this.#forward.value = [...this.#forward.peek(), here];
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.#forward.value = stack.slice(0, -1);
|
|
175
|
+
this.#back.value = [...this.#back.peek(), here];
|
|
176
|
+
}
|
|
177
|
+
this.#slide(back ? -1 : 1, () => (this.#path.value = to));
|
|
178
|
+
}
|
|
179
|
+
/** Swaps the level with a slide: the new list from the side it's going to, the old one away. */
|
|
180
|
+
#slide(direction, change) {
|
|
181
|
+
const list = this.querySelector(".aster-drill-list");
|
|
182
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
183
|
+
if (list && !reduce) {
|
|
184
|
+
const ghost = list.cloneNode(true);
|
|
185
|
+
ghost.inert = true;
|
|
186
|
+
ghost.setAttribute("aria-hidden", "true");
|
|
187
|
+
ghost.classList.add("aster-drill-ghost");
|
|
188
|
+
ghost.removeAttribute("aria-labelledby");
|
|
189
|
+
ghost.scrollTop = list.scrollTop;
|
|
190
|
+
this.#ghost?.remove();
|
|
191
|
+
this.#ghost = ghost;
|
|
192
|
+
}
|
|
193
|
+
this.#direction = direction;
|
|
194
|
+
change();
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
this.querySelector(".aster-drill-heading")?.focus();
|
|
197
|
+
const incoming = this.querySelector(".aster-drill-list");
|
|
198
|
+
const outgoing = this.#ghost;
|
|
199
|
+
this.#ghost = undefined;
|
|
200
|
+
if (!incoming)
|
|
201
|
+
return;
|
|
202
|
+
incoming.scrollTop = 0;
|
|
203
|
+
if (!outgoing)
|
|
204
|
+
return;
|
|
205
|
+
incoming.parentElement?.append(outgoing);
|
|
206
|
+
const back = this.#direction < 0;
|
|
207
|
+
outgoing.style.zIndex = back ? "2" : "0";
|
|
208
|
+
const timing = { duration: 240, easing: "cubic-bezier(.22,.7,.25,1)" };
|
|
209
|
+
const animations = [
|
|
210
|
+
incoming.animate([{ transform: `translateX(${back ? "-24%" : "100%"})`, opacity: 0.65 }, { transform: "none", opacity: 1 }], timing),
|
|
211
|
+
outgoing.animate([{ transform: "none" }, { transform: `translateX(${back ? "100%" : "-24%"})`, opacity: 0.65 }], timing),
|
|
212
|
+
];
|
|
213
|
+
void Promise.all(animations.map((a) => a.finished.catch(() => { }))).then(() => outgoing.remove());
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
#key(e) {
|
|
217
|
+
if (e.key === "Escape") {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
this.emit("cancel");
|
|
220
|
+
}
|
|
221
|
+
else if (e.key === "Backspace" && !e.target.matches("input, textarea") && this.#path.peek().length) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
const path = this.#path.peek();
|
|
224
|
+
this.#move(path.slice(0, -1), -1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
define("aster-tree-navigator", TreeNavigator);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { html } from "lit-html";
|
|
2
|
+
/** Shared title/chrome geometry for floating windows and dashboard portlets. */
|
|
3
|
+
export function windowBar(id, heading, actions, drag) {
|
|
4
|
+
return html `<div class="aster-window-bar" @pointerdown=${drag}>
|
|
5
|
+
<h2 id=${id}>${heading}</h2>${actions}
|
|
6
|
+
</div>`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
+
export type WindowState = "normal" | "maximized" | "minimized";
|
|
3
|
+
export interface WindowBounds {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface WindowPlacement {
|
|
10
|
+
state: WindowState;
|
|
11
|
+
bounds?: WindowBounds;
|
|
12
|
+
restoreMaximized?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Non-modal chrome; content ownership stays with the host page. */
|
|
15
|
+
export declare class Window extends AsterElement {
|
|
16
|
+
#private;
|
|
17
|
+
get heading(): string;
|
|
18
|
+
set heading(v: string);
|
|
19
|
+
get content(): View;
|
|
20
|
+
set content(v: View);
|
|
21
|
+
get open(): boolean;
|
|
22
|
+
get state(): WindowState;
|
|
23
|
+
get active(): boolean;
|
|
24
|
+
get managed(): boolean;
|
|
25
|
+
set managed(v: boolean);
|
|
26
|
+
get preserveContent(): boolean;
|
|
27
|
+
set preserveContent(v: boolean);
|
|
28
|
+
get closing(): boolean;
|
|
29
|
+
get beforeClose(): (() => boolean | Promise<boolean>) | undefined;
|
|
30
|
+
set beforeClose(v: (() => boolean | Promise<boolean>) | undefined);
|
|
31
|
+
connectedCallback(): void;
|
|
32
|
+
show(opener?: HTMLElement | null): void;
|
|
33
|
+
/** Activates chrome without reparenting the content or recreating its models. */
|
|
34
|
+
activate(focus?: boolean): void;
|
|
35
|
+
deactivate(): void;
|
|
36
|
+
close(): Promise<boolean>;
|
|
37
|
+
toggle(opener?: HTMLElement | null): void;
|
|
38
|
+
minimize(): void;
|
|
39
|
+
maximize(): void;
|
|
40
|
+
restore(): void;
|
|
41
|
+
placement(): WindowPlacement;
|
|
42
|
+
/** Invalid persisted data is rejected atomically. CSS limits size to the viewport. */
|
|
43
|
+
setPlacement(value: WindowPlacement): void;
|
|
44
|
+
protected updated(): void;
|
|
45
|
+
protected render(): View;
|
|
46
|
+
}
|