@getaster/elements 0.0.0-stage → 0.1.0-preview.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/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 +2584 -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 +1145 -0
- package/styles/base.css +86 -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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer drags that work on touch screens too.
|
|
3
|
+
*
|
|
4
|
+
* A mouse lifts a drag once it moves `distance` pixels. A touch or pen
|
|
5
|
+
* lifts it by holding still for `HOLD_MS`; moving more than `HOLD_SLOP`
|
|
6
|
+
* first is a scroll, left to the browser, and the press is forgotten.
|
|
7
|
+
* `touch-action` is fixed when a touch starts, so it stays at the default
|
|
8
|
+
* (scrolling works everywhere) and a lifted drag holds the page still by
|
|
9
|
+
* cancelling its `touchmove`s instead. While a drag is lifted, its
|
|
10
|
+
* scroller follows it near the top and bottom edges.
|
|
11
|
+
*
|
|
12
|
+
* Moves are listened for on the document by pointer id, so they keep
|
|
13
|
+
* arriving when the row under the finger is re-rendered away; only a mouse
|
|
14
|
+
* takes pointer capture.
|
|
15
|
+
*/
|
|
16
|
+
/** How long a touch or pen holds still to lift a drag. */
|
|
17
|
+
export const HOLD_MS = 400;
|
|
18
|
+
/** How far a held touch or pen may drift and still be holding still. */
|
|
19
|
+
export const HOLD_SLOP = 8;
|
|
20
|
+
/** The auto-scroll band at a scroller's top and bottom, and the most it scrolls per frame. */
|
|
21
|
+
const EDGE = 48;
|
|
22
|
+
const SPEED = 16;
|
|
23
|
+
/** Whether a press lifts by holding still (touch, pen) rather than by moving (mouse). */
|
|
24
|
+
export function holds(e) {
|
|
25
|
+
return e.pointerType === "touch" || e.pointerType === "pen";
|
|
26
|
+
}
|
|
27
|
+
/** Follows the press `down` until it lifts a drag and ends, or turns out to be a tap or a scroll. */
|
|
28
|
+
export function pressDrag(down, options, handlers) {
|
|
29
|
+
const id = down.pointerId;
|
|
30
|
+
const hold = holds(down);
|
|
31
|
+
const from = { x: down.clientX, y: down.clientY };
|
|
32
|
+
let at = from;
|
|
33
|
+
let lifted = false;
|
|
34
|
+
let frame = 0;
|
|
35
|
+
const scroller = options.scroller?.() ?? scrollerOf(options.capture);
|
|
36
|
+
const lift = () => {
|
|
37
|
+
lifted = true;
|
|
38
|
+
if (!hold)
|
|
39
|
+
options.capture.setPointerCapture(id);
|
|
40
|
+
handlers.lift();
|
|
41
|
+
handlers.move(at.x, at.y);
|
|
42
|
+
follow();
|
|
43
|
+
};
|
|
44
|
+
const timer = hold ? setTimeout(lift, HOLD_MS) : undefined;
|
|
45
|
+
/** Scrolls while the pointer is in an edge band, re-reporting its place as the content moves. */
|
|
46
|
+
const follow = () => {
|
|
47
|
+
if (frame || !scroller)
|
|
48
|
+
return;
|
|
49
|
+
const step = () => {
|
|
50
|
+
frame = 0;
|
|
51
|
+
const v = lifted ? speed(scroller, at.y, options.inset?.() ?? 0) : 0;
|
|
52
|
+
if (!v)
|
|
53
|
+
return;
|
|
54
|
+
const before = scroller.scrollTop;
|
|
55
|
+
scroller.scrollTop += v;
|
|
56
|
+
if (scroller.scrollTop === before)
|
|
57
|
+
return;
|
|
58
|
+
handlers.move(at.x, at.y);
|
|
59
|
+
frame = requestAnimationFrame(step);
|
|
60
|
+
};
|
|
61
|
+
frame = requestAnimationFrame(step);
|
|
62
|
+
};
|
|
63
|
+
const move = (e) => {
|
|
64
|
+
if (e.pointerId !== id)
|
|
65
|
+
return;
|
|
66
|
+
at = { x: e.clientX, y: e.clientY };
|
|
67
|
+
if (!lifted) {
|
|
68
|
+
const dx = at.x - from.x;
|
|
69
|
+
const dy = at.y - from.y;
|
|
70
|
+
if (hold) {
|
|
71
|
+
if (Math.hypot(dx, dy) > HOLD_SLOP)
|
|
72
|
+
stop(false);
|
|
73
|
+
}
|
|
74
|
+
else if ((options.vertical ? Math.abs(dy) : Math.hypot(dx, dy)) >= options.distance)
|
|
75
|
+
lift();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
handlers.move(at.x, at.y);
|
|
79
|
+
follow();
|
|
80
|
+
};
|
|
81
|
+
const up = (e) => e.pointerId === id && stop(true);
|
|
82
|
+
const cancel = (e) => e.pointerId === id && stop(false);
|
|
83
|
+
const still = (e) => lifted && e.preventDefault();
|
|
84
|
+
// A held press would otherwise open the context menu or start selecting text.
|
|
85
|
+
const quiet = (e) => e.preventDefault();
|
|
86
|
+
const escape = (e) => {
|
|
87
|
+
if (e.key !== "Escape")
|
|
88
|
+
return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
stop(false);
|
|
92
|
+
};
|
|
93
|
+
const listeners = [
|
|
94
|
+
[document, "pointermove", move],
|
|
95
|
+
[document, "pointerup", up],
|
|
96
|
+
[document, "pointercancel", cancel],
|
|
97
|
+
];
|
|
98
|
+
if (hold) {
|
|
99
|
+
listeners.push([document, "touchmove", still, { passive: false }], [window, "contextmenu", quiet, { capture: true }], [document, "selectstart", quiet, { capture: true }]);
|
|
100
|
+
}
|
|
101
|
+
if (options.escape)
|
|
102
|
+
listeners.push([document, "keydown", escape, { capture: true }]);
|
|
103
|
+
for (const [target, type, fn, o] of listeners)
|
|
104
|
+
target.addEventListener(type, fn, o);
|
|
105
|
+
function stop(drop) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
cancelAnimationFrame(frame);
|
|
108
|
+
frame = 0;
|
|
109
|
+
for (const [target, type, fn, o] of listeners)
|
|
110
|
+
target.removeEventListener(type, fn, o);
|
|
111
|
+
if (options.capture.hasPointerCapture(id))
|
|
112
|
+
options.capture.releasePointerCapture(id);
|
|
113
|
+
if (lifted)
|
|
114
|
+
handlers.end(drop);
|
|
115
|
+
lifted = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Pixels per frame to scroll `el` with the pointer at `y`: negative up, 0 outside the edge bands. */
|
|
119
|
+
function speed(el, y, inset) {
|
|
120
|
+
const page = el === document.scrollingElement;
|
|
121
|
+
const top = (page ? 0 : el.getBoundingClientRect().top) + inset;
|
|
122
|
+
const bottom = page ? innerHeight : el.getBoundingClientRect().bottom;
|
|
123
|
+
const band = Math.min(EDGE, (bottom - top) / 4);
|
|
124
|
+
if (band <= 0 || y < top)
|
|
125
|
+
return 0;
|
|
126
|
+
if (y < top + band)
|
|
127
|
+
return -Math.ceil((SPEED * (top + band - y)) / band);
|
|
128
|
+
if (y > bottom - band)
|
|
129
|
+
return Math.ceil((SPEED * Math.min(band, y - (bottom - band))) / band);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
function scrollerOf(el) {
|
|
133
|
+
for (let n = el.parentElement; n; n = n.parentElement) {
|
|
134
|
+
const overflow = getComputedStyle(n).overflowY;
|
|
135
|
+
if ((overflow === "auto" || overflow === "scroll") && n.scrollHeight > n.clientHeight)
|
|
136
|
+
return n;
|
|
137
|
+
}
|
|
138
|
+
return document.scrollingElement ?? document.documentElement;
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReadonlySignal, type Signal } from "@preact/signals-core";
|
|
2
|
+
import type { PageScope } from "@getaster/data";
|
|
3
|
+
import { nothing, type TemplateResult } from "lit-html";
|
|
4
|
+
export type View = TemplateResult | typeof nothing;
|
|
5
|
+
/** A plain value or a signal of one; elements read either. */
|
|
6
|
+
export type Bindable<T> = T | ReadonlySignal<T>;
|
|
7
|
+
export declare function read<T>(value: Bindable<T>): T;
|
|
8
|
+
/**
|
|
9
|
+
* The base of every Aster element: light DOM, rendered with lit-html from
|
|
10
|
+
* `render()`. Any signal `render()` reads schedules a re-render.
|
|
11
|
+
*
|
|
12
|
+
* Only signals read while `render()` runs are tracked. Callbacks lit calls
|
|
13
|
+
* later, during the commit (such as `repeat`'s template), are not, so keyed
|
|
14
|
+
* lists use `each`, which builds every item's view first.
|
|
15
|
+
*
|
|
16
|
+
* Invariant: the template is computed synchronously when its signals change,
|
|
17
|
+
* and committed to the DOM once per microtask, so a batch of changes costs
|
|
18
|
+
* one commit. The first render on connect commits immediately.
|
|
19
|
+
*
|
|
20
|
+
* An element defined after it was created (a lazily loaded page) may have
|
|
21
|
+
* had properties set while it was a plain element; on first connect they
|
|
22
|
+
* are re-set through the class's setters.
|
|
23
|
+
*/
|
|
24
|
+
export declare abstract class AsterElement extends HTMLElement {
|
|
25
|
+
#private;
|
|
26
|
+
protected abstract render(): View;
|
|
27
|
+
/** Runs after each commit; geometry reads belong here. */
|
|
28
|
+
protected updated(): void;
|
|
29
|
+
connectedCallback(): void;
|
|
30
|
+
disconnectedCallback(): void;
|
|
31
|
+
/** A signal-backed property: `#x = this.prop(1)` and get/set through it. */
|
|
32
|
+
protected prop<T>(initial: T): Signal<T>;
|
|
33
|
+
/** Disposed when the element disconnects. */
|
|
34
|
+
protected onDisconnect(cleanup: () => void): void;
|
|
35
|
+
/** Listens on `target` until the element disconnects. */
|
|
36
|
+
protected listen<K extends keyof WindowEventMap>(target: EventTarget, type: K, handler: (event: WindowEventMap[K]) => void, options?: AddEventListenerOptions): void;
|
|
37
|
+
protected emit<T>(type: string, detail?: T): boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function define(name: string, ctor: CustomElementConstructor): void;
|
|
40
|
+
/** Makes `scope` the page scope for `el` and everything inside it. */
|
|
41
|
+
export declare function provideScope(el: Element, scope: PageScope): void;
|
|
42
|
+
/** The page scope `el` is in: the nearest one provided on it or an ancestor. */
|
|
43
|
+
export declare function scopeOf(el: Element): PageScope | undefined;
|
|
44
|
+
/** A document-unique id for ARIA references. */
|
|
45
|
+
export declare function uid(prefix: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* A keyed list for templates. Each item's view is built now, inside the
|
|
48
|
+
* caller's `render()`, so the signals it reads are tracked; lit then only
|
|
49
|
+
* moves and updates the finished views.
|
|
50
|
+
*/
|
|
51
|
+
export declare function each<T>(items: readonly T[], key: (item: T) => string, view: (item: T) => View): unknown;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { computed, effect, signal, untracked } from "@preact/signals-core";
|
|
2
|
+
import { nothing, render } from "lit-html";
|
|
3
|
+
import { repeat } from "lit-html/directives/repeat.js";
|
|
4
|
+
export function read(value) {
|
|
5
|
+
return isSignal(value) ? value.value : value;
|
|
6
|
+
}
|
|
7
|
+
function isSignal(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && "peek" in value && "subscribe" in value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The base of every Aster element: light DOM, rendered with lit-html from
|
|
12
|
+
* `render()`. Any signal `render()` reads schedules a re-render.
|
|
13
|
+
*
|
|
14
|
+
* Only signals read while `render()` runs are tracked. Callbacks lit calls
|
|
15
|
+
* later, during the commit (such as `repeat`'s template), are not, so keyed
|
|
16
|
+
* lists use `each`, which builds every item's view first.
|
|
17
|
+
*
|
|
18
|
+
* Invariant: the template is computed synchronously when its signals change,
|
|
19
|
+
* and committed to the DOM once per microtask, so a batch of changes costs
|
|
20
|
+
* one commit. The first render on connect commits immediately.
|
|
21
|
+
*
|
|
22
|
+
* An element defined after it was created (a lazily loaded page) may have
|
|
23
|
+
* had properties set while it was a plain element; on first connect they
|
|
24
|
+
* are re-set through the class's setters.
|
|
25
|
+
*/
|
|
26
|
+
export class AsterElement extends HTMLElement {
|
|
27
|
+
#dispose;
|
|
28
|
+
#queued = false;
|
|
29
|
+
#view;
|
|
30
|
+
#cleanups = [];
|
|
31
|
+
/** Runs after each commit; geometry reads belong here. */
|
|
32
|
+
updated() { }
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
this.#upgradeProperties();
|
|
35
|
+
const view = computed(() => this.render());
|
|
36
|
+
this.#view = view;
|
|
37
|
+
let first = true;
|
|
38
|
+
this.#dispose = effect(() => {
|
|
39
|
+
view.value;
|
|
40
|
+
if (first) {
|
|
41
|
+
first = false;
|
|
42
|
+
// Children connecting during the commit must not subscribe this effect.
|
|
43
|
+
untracked(() => this.#commit());
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.#schedule();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
disconnectedCallback() {
|
|
51
|
+
this.#dispose?.();
|
|
52
|
+
this.#dispose = undefined;
|
|
53
|
+
for (const cleanup of this.#cleanups.splice(0))
|
|
54
|
+
cleanup();
|
|
55
|
+
}
|
|
56
|
+
/** A signal-backed property: `#x = this.prop(1)` and get/set through it. */
|
|
57
|
+
prop(initial) {
|
|
58
|
+
return signal(initial);
|
|
59
|
+
}
|
|
60
|
+
/** Disposed when the element disconnects. */
|
|
61
|
+
onDisconnect(cleanup) {
|
|
62
|
+
this.#cleanups.push(cleanup);
|
|
63
|
+
}
|
|
64
|
+
/** Listens on `target` until the element disconnects. */
|
|
65
|
+
listen(target, type, handler, options) {
|
|
66
|
+
target.addEventListener(type, handler, options);
|
|
67
|
+
this.onDisconnect(() => target.removeEventListener(type, handler, options));
|
|
68
|
+
}
|
|
69
|
+
emit(type, detail) {
|
|
70
|
+
return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, cancelable: true }));
|
|
71
|
+
}
|
|
72
|
+
#upgradeProperties() {
|
|
73
|
+
for (const name of Object.keys(this)) {
|
|
74
|
+
let proto = Object.getPrototypeOf(this);
|
|
75
|
+
while (proto && proto !== HTMLElement.prototype && !Object.getOwnPropertyDescriptor(proto, name)?.set) {
|
|
76
|
+
proto = Object.getPrototypeOf(proto);
|
|
77
|
+
}
|
|
78
|
+
if (!proto || proto === HTMLElement.prototype)
|
|
79
|
+
continue;
|
|
80
|
+
const value = this[name];
|
|
81
|
+
delete this[name];
|
|
82
|
+
this[name] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
#schedule() {
|
|
86
|
+
if (this.#queued)
|
|
87
|
+
return;
|
|
88
|
+
this.#queued = true;
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
this.#queued = false;
|
|
91
|
+
if (this.#dispose)
|
|
92
|
+
this.#commit();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
#commit() {
|
|
96
|
+
if (!this.#view)
|
|
97
|
+
return;
|
|
98
|
+
render(this.#view.peek(), this, { host: this });
|
|
99
|
+
this.updated();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function define(name, ctor) {
|
|
103
|
+
if (!customElements.get(name))
|
|
104
|
+
customElements.define(name, ctor);
|
|
105
|
+
}
|
|
106
|
+
const SCOPE = Symbol("aster-scope");
|
|
107
|
+
/** Makes `scope` the page scope for `el` and everything inside it. */
|
|
108
|
+
export function provideScope(el, scope) {
|
|
109
|
+
el[SCOPE] = scope;
|
|
110
|
+
}
|
|
111
|
+
/** The page scope `el` is in: the nearest one provided on it or an ancestor. */
|
|
112
|
+
export function scopeOf(el) {
|
|
113
|
+
for (let n = el; n; n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null)) {
|
|
114
|
+
const scope = n[SCOPE];
|
|
115
|
+
if (scope)
|
|
116
|
+
return scope;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
let ids = 0;
|
|
121
|
+
/** A document-unique id for ARIA references. */
|
|
122
|
+
export function uid(prefix) {
|
|
123
|
+
return `${prefix}-${(ids++).toString(36)}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A keyed list for templates. Each item's view is built now, inside the
|
|
127
|
+
* caller's `render()`, so the signals it reads are tracked; lit then only
|
|
128
|
+
* moves and updates the finished views.
|
|
129
|
+
*/
|
|
130
|
+
export function each(items, key, view) {
|
|
131
|
+
const built = items.map((item) => ({ key: key(item), view: view(item) }));
|
|
132
|
+
return repeat(built, (b) => b.key, (b) => b.view);
|
|
133
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BreadcrumbItem {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly href?: string;
|
|
5
|
+
readonly activate?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const Breadcrumbs: import("../index.ts").DeclaredElement<{
|
|
8
|
+
readonly items: {
|
|
9
|
+
readonly type: import("../index.ts").CompositeType<readonly BreadcrumbItem[]>;
|
|
10
|
+
readonly initial: () => readonly BreadcrumbItem[];
|
|
11
|
+
readonly description: string;
|
|
12
|
+
};
|
|
13
|
+
readonly label: {
|
|
14
|
+
readonly type: import("../index.ts").CompositeType<string>;
|
|
15
|
+
readonly initial: () => string;
|
|
16
|
+
readonly description: string;
|
|
17
|
+
};
|
|
18
|
+
readonly maxVisible: {
|
|
19
|
+
readonly type: import("../index.ts").CompositeType<number>;
|
|
20
|
+
readonly initial: () => number;
|
|
21
|
+
readonly description: string;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
|
+
import { each } from "../base/element.js";
|
|
3
|
+
import { defineElement, elementProperty } from "../composition/element.js";
|
|
4
|
+
import { compositeTypes as t } from "../composition/contracts.js";
|
|
5
|
+
import { sidebarHref } from "../sidebar/sidebar.js";
|
|
6
|
+
import { Menu } from "../menu/menu.js";
|
|
7
|
+
const items = t.custom("breadcrumbs with unique IDs, labels and safe links", (v) => {
|
|
8
|
+
if (!Array.isArray(v))
|
|
9
|
+
return false;
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
return v.every(i => {
|
|
12
|
+
if (!i || typeof i.id !== "string" || !i.id || ids.has(i.id) || typeof i.label !== "string" || !i.label.trim())
|
|
13
|
+
return false;
|
|
14
|
+
ids.add(i.id);
|
|
15
|
+
return (i.href === undefined || typeof i.href === "string" && sidebarHref(i.href)) && (i.activate === undefined || typeof i.activate === "function") && !(i.href && i.activate);
|
|
16
|
+
});
|
|
17
|
+
}, { type: "array", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string" }, label: { type: "string" }, href: { type: "string" }, activate: { asterType: "function" } }, additionalProperties: false } });
|
|
18
|
+
export const Breadcrumbs = defineElement({
|
|
19
|
+
tag: "aster-breadcrumbs",
|
|
20
|
+
properties: {
|
|
21
|
+
items: elementProperty(items, () => [], "Ancestor locations"),
|
|
22
|
+
label: elementProperty(t.string, () => "Breadcrumb", "Navigation name"),
|
|
23
|
+
maxVisible: elementProperty(t.custom("at least three visible items", (v) => Number.isSafeInteger(v) && v >= 3, { type: "integer", minimum: 3 }), () => 4, "Visible item budget including overflow"),
|
|
24
|
+
},
|
|
25
|
+
render: (p, element) => {
|
|
26
|
+
const activate = (item, e) => {
|
|
27
|
+
if (e && (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0))
|
|
28
|
+
return;
|
|
29
|
+
if (!element.dispatchEvent(new CustomEvent("navigate", { detail: item.id, bubbles: true, cancelable: true }))) {
|
|
30
|
+
e?.preventDefault();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
item.activate?.();
|
|
34
|
+
};
|
|
35
|
+
const overflow = p.items.length > p.maxVisible;
|
|
36
|
+
const omitted = overflow ? p.items.slice(1, p.items.length - (p.maxVisible - 2)) : [];
|
|
37
|
+
const shown = overflow ? [p.items[0], ...p.items.slice(-(p.maxVisible - 2))] : p.items;
|
|
38
|
+
return html `<nav class="aster-breadcrumbs" aria-label=${p.label}><ol>
|
|
39
|
+
${each(shown, i => i.id, item => html `<li>
|
|
40
|
+
${item === p.items.at(-1) ? html `<span aria-current="page" title=${item.label}>${item.label}</span>`
|
|
41
|
+
: item.href ? html `<a href=${item.href} title=${item.label} @click=${(e) => activate(item, e)}>${item.label}</a>`
|
|
42
|
+
: html `<button type="button" title=${item.label} @click=${() => activate(item)}>${item.label}</button>`}
|
|
43
|
+
${overflow && item === p.items[0] ? html `<span class="aster-breadcrumb-overflow"><button type="button" aria-label="Show ancestors" aria-haspopup="menu" @click=${(e) => {
|
|
44
|
+
const menu = element.querySelector("aster-menu");
|
|
45
|
+
const choices = omitted.map(i => i.href ? { kind: "link", label: i.label, href: i.href, activate: event => activate(i, event) } : { label: i.label, run: () => activate(i) });
|
|
46
|
+
menu.openFrom(e.currentTarget, choices, "Ancestors");
|
|
47
|
+
}}>…</button></span>` : nothing}
|
|
48
|
+
</li>`)}
|
|
49
|
+
</ol></nav><aster-menu></aster-menu>`;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AsterElement, type Bindable, type View } from "../base/element.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A button with every state: rest, hover, pressed, focus, busy and
|
|
4
|
+
* disabled. Busy and confirmation swap the label in place; every label is
|
|
5
|
+
* stacked in one grid cell, so the width never changes. While busy, clicks
|
|
6
|
+
* are ignored and a submit doesn't fire.
|
|
7
|
+
*
|
|
8
|
+
* Prefer `reason` to disabling: the button stays focusable, says why in its
|
|
9
|
+
* title, and its click is swallowed.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Button extends AsterElement {
|
|
12
|
+
#private;
|
|
13
|
+
get label(): string;
|
|
14
|
+
set label(v: string);
|
|
15
|
+
/** Shown while busy, such as "Inviting…". */
|
|
16
|
+
get busyLabel(): string | undefined;
|
|
17
|
+
set busyLabel(v: string | undefined);
|
|
18
|
+
/** Shown briefly by `confirm()`, such as "Copied". */
|
|
19
|
+
get confirmLabel(): string | undefined;
|
|
20
|
+
set confirmLabel(v: string | undefined);
|
|
21
|
+
get busy(): Bindable<boolean>;
|
|
22
|
+
set busy(v: Bindable<boolean>);
|
|
23
|
+
get variant(): "secondary" | "primary";
|
|
24
|
+
set variant(v: "secondary" | "primary");
|
|
25
|
+
get type(): "button" | "submit";
|
|
26
|
+
set type(v: "button" | "submit");
|
|
27
|
+
get icon(): View;
|
|
28
|
+
set icon(v: View);
|
|
29
|
+
/** Why the button can't act now. Setting it disables the button without removing it from the tab order. */
|
|
30
|
+
get reason(): string | undefined;
|
|
31
|
+
set reason(v: string | undefined);
|
|
32
|
+
connectedCallback(): void;
|
|
33
|
+
/** Shows the confirm label in place for a moment. */
|
|
34
|
+
confirm(ms?: number): void;
|
|
35
|
+
focus(options?: FocusOptions): void;
|
|
36
|
+
protected render(): View;
|
|
37
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
|
+
import { AsterElement, define, read } from "../base/element.js";
|
|
3
|
+
/**
|
|
4
|
+
* A button with every state: rest, hover, pressed, focus, busy and
|
|
5
|
+
* disabled. Busy and confirmation swap the label in place; every label is
|
|
6
|
+
* stacked in one grid cell, so the width never changes. While busy, clicks
|
|
7
|
+
* are ignored and a submit doesn't fire.
|
|
8
|
+
*
|
|
9
|
+
* Prefer `reason` to disabling: the button stays focusable, says why in its
|
|
10
|
+
* title, and its click is swallowed.
|
|
11
|
+
*/
|
|
12
|
+
export class Button extends AsterElement {
|
|
13
|
+
#label = this.prop("");
|
|
14
|
+
#busyLabel = this.prop(undefined);
|
|
15
|
+
#confirmLabel = this.prop(undefined);
|
|
16
|
+
#busy = this.prop(false);
|
|
17
|
+
#confirming = this.prop(false);
|
|
18
|
+
#variant = this.prop("secondary");
|
|
19
|
+
#type = this.prop("button");
|
|
20
|
+
#icon = this.prop(nothing);
|
|
21
|
+
#reason = this.prop(undefined);
|
|
22
|
+
#confirmTimer;
|
|
23
|
+
get label() {
|
|
24
|
+
return this.#label.value;
|
|
25
|
+
}
|
|
26
|
+
set label(v) {
|
|
27
|
+
this.#label.value = v;
|
|
28
|
+
}
|
|
29
|
+
/** Shown while busy, such as "Inviting…". */
|
|
30
|
+
get busyLabel() {
|
|
31
|
+
return this.#busyLabel.value;
|
|
32
|
+
}
|
|
33
|
+
set busyLabel(v) {
|
|
34
|
+
this.#busyLabel.value = v;
|
|
35
|
+
}
|
|
36
|
+
/** Shown briefly by `confirm()`, such as "Copied". */
|
|
37
|
+
get confirmLabel() {
|
|
38
|
+
return this.#confirmLabel.value;
|
|
39
|
+
}
|
|
40
|
+
set confirmLabel(v) {
|
|
41
|
+
this.#confirmLabel.value = v;
|
|
42
|
+
}
|
|
43
|
+
get busy() {
|
|
44
|
+
return this.#busy.value;
|
|
45
|
+
}
|
|
46
|
+
set busy(v) {
|
|
47
|
+
this.#busy.value = v;
|
|
48
|
+
}
|
|
49
|
+
get variant() {
|
|
50
|
+
return this.#variant.value;
|
|
51
|
+
}
|
|
52
|
+
set variant(v) {
|
|
53
|
+
this.#variant.value = v;
|
|
54
|
+
}
|
|
55
|
+
get type() {
|
|
56
|
+
return this.#type.value;
|
|
57
|
+
}
|
|
58
|
+
set type(v) {
|
|
59
|
+
this.#type.value = v;
|
|
60
|
+
}
|
|
61
|
+
get icon() {
|
|
62
|
+
return this.#icon.value;
|
|
63
|
+
}
|
|
64
|
+
set icon(v) {
|
|
65
|
+
this.#icon.value = v;
|
|
66
|
+
}
|
|
67
|
+
/** Why the button can't act now. Setting it disables the button without removing it from the tab order. */
|
|
68
|
+
get reason() {
|
|
69
|
+
return this.#reason.value;
|
|
70
|
+
}
|
|
71
|
+
set reason(v) {
|
|
72
|
+
this.#reason.value = v;
|
|
73
|
+
}
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
super.connectedCallback();
|
|
76
|
+
this.listen(this, "click", (e) => this.#guard(e), { capture: true });
|
|
77
|
+
}
|
|
78
|
+
/** Shows the confirm label in place for a moment. */
|
|
79
|
+
confirm(ms = 1600) {
|
|
80
|
+
clearTimeout(this.#confirmTimer);
|
|
81
|
+
this.#confirming.value = true;
|
|
82
|
+
this.#confirmTimer = setTimeout(() => (this.#confirming.value = false), ms);
|
|
83
|
+
}
|
|
84
|
+
focus(options) {
|
|
85
|
+
this.querySelector("button")?.focus(options);
|
|
86
|
+
}
|
|
87
|
+
render() {
|
|
88
|
+
const busy = read(this.#busy.value);
|
|
89
|
+
const confirming = this.#confirming.value;
|
|
90
|
+
const labels = [this.#label.value, this.#busyLabel.value, this.#confirmLabel.value];
|
|
91
|
+
const showing = busy && labels[1] ? 1 : confirming && labels[2] ? 2 : 0;
|
|
92
|
+
const reason = this.#reason.value;
|
|
93
|
+
return html `<button class="aster-button" type=${this.#type.value} data-variant=${this.#variant.value}
|
|
94
|
+
aria-busy=${busy ? "true" : "false"} aria-disabled=${reason ? "true" : nothing} title=${reason ?? nothing}>
|
|
95
|
+
${this.#icon.value}
|
|
96
|
+
<span class="aster-label-swap">
|
|
97
|
+
${labels.map((l, i) => (l === undefined ? nothing : html `<span ?data-off=${i !== showing}>${l}</span>`))}
|
|
98
|
+
</span>
|
|
99
|
+
</button>`;
|
|
100
|
+
}
|
|
101
|
+
#guard(e) {
|
|
102
|
+
if (read(this.#busy.peek()) || this.#reason.peek()) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
e.stopImmediatePropagation();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
define("aster-button", Button);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A group with one caret: a bar that slides to the selected or current
|
|
3
|
+
* descendant. `axis="x"` puts it under the item (tabs); the default `y` puts
|
|
4
|
+
* it at the item's left edge (nav, lists).
|
|
5
|
+
*
|
|
6
|
+
* The caret's position is read from the item's offsets after layout and
|
|
7
|
+
* written as CSS variables; CSS draws and animates it. The group is the
|
|
8
|
+
* caret's containing block, so it must stay `position: relative`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CaretGroup extends HTMLElement {
|
|
11
|
+
#private;
|
|
12
|
+
connectedCallback(): void;
|
|
13
|
+
disconnectedCallback(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { define } from "../base/element.js";
|
|
2
|
+
const SELECTED = '[aria-current="true"], [aria-current="page"], [aria-selected="true"]';
|
|
3
|
+
/**
|
|
4
|
+
* A group with one caret: a bar that slides to the selected or current
|
|
5
|
+
* descendant. `axis="x"` puts it under the item (tabs); the default `y` puts
|
|
6
|
+
* it at the item's left edge (nav, lists).
|
|
7
|
+
*
|
|
8
|
+
* The caret's position is read from the item's offsets after layout and
|
|
9
|
+
* written as CSS variables; CSS draws and animates it. The group is the
|
|
10
|
+
* caret's containing block, so it must stay `position: relative`.
|
|
11
|
+
*/
|
|
12
|
+
export class CaretGroup extends HTMLElement {
|
|
13
|
+
#caret = document.createElement("span");
|
|
14
|
+
#mutations;
|
|
15
|
+
#resize;
|
|
16
|
+
#frame = 0;
|
|
17
|
+
#placed = false;
|
|
18
|
+
connectedCallback() {
|
|
19
|
+
this.#caret.className = "aster-caret";
|
|
20
|
+
this.#caret.setAttribute("aria-hidden", "true");
|
|
21
|
+
if (this.#caret.parentNode !== this)
|
|
22
|
+
this.prepend(this.#caret);
|
|
23
|
+
this.#mutations = new MutationObserver(() => this.#queue());
|
|
24
|
+
this.#mutations.observe(this, {
|
|
25
|
+
subtree: true,
|
|
26
|
+
childList: true,
|
|
27
|
+
attributes: true,
|
|
28
|
+
attributeFilter: ["aria-current", "aria-selected", "hidden"],
|
|
29
|
+
});
|
|
30
|
+
this.#resize = new ResizeObserver(() => this.#queue(false));
|
|
31
|
+
this.#resize.observe(this);
|
|
32
|
+
void document.fonts?.ready.then(() => this.#queue(false));
|
|
33
|
+
this.#queue(false);
|
|
34
|
+
}
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
this.#mutations?.disconnect();
|
|
37
|
+
this.#resize?.disconnect();
|
|
38
|
+
cancelAnimationFrame(this.#frame);
|
|
39
|
+
this.#placed = false;
|
|
40
|
+
}
|
|
41
|
+
#queue(animate = true) {
|
|
42
|
+
cancelAnimationFrame(this.#frame);
|
|
43
|
+
this.#frame = requestAnimationFrame(() => this.#place(animate && this.#placed));
|
|
44
|
+
}
|
|
45
|
+
#place(animate) {
|
|
46
|
+
const item = [...this.querySelectorAll(SELECTED)].find((el) => el.closest("aster-caret-group") === this && el.offsetParent !== null);
|
|
47
|
+
const style = this.#caret.style;
|
|
48
|
+
if (!item) {
|
|
49
|
+
style.setProperty("--caret-opacity", "0");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.#caret.toggleAttribute("data-instant", !animate);
|
|
53
|
+
const { pos, size } = this.#offsets(item);
|
|
54
|
+
style.setProperty("--caret-opacity", "1");
|
|
55
|
+
style.setProperty("--caret-pos", `${pos}px`);
|
|
56
|
+
style.setProperty("--caret-size", `${size}px`);
|
|
57
|
+
this.#placed = true;
|
|
58
|
+
}
|
|
59
|
+
/** The item's position within this group, through any positioned ancestors in between. */
|
|
60
|
+
#offsets(item) {
|
|
61
|
+
const x = this.getAttribute("axis") === "x";
|
|
62
|
+
let pos = 0;
|
|
63
|
+
for (let el = item; el && el !== this; el = el.offsetParent) {
|
|
64
|
+
pos += x ? el.offsetLeft : el.offsetTop;
|
|
65
|
+
if (!this.contains(el.offsetParent))
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return { pos, size: x ? item.offsetWidth : item.offsetHeight };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
define("aster-caret-group", CaretGroup);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
+
export interface Slide {
|
|
3
|
+
readonly id?: string;
|
|
4
|
+
/** Names the slide, after its position ("2 of 5: Brand refresh"). */
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly view: View;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A row of cards that snaps one at a time, the next one peeking in (the APG
|
|
10
|
+
* carousel pattern, without rotation): Previous and Next, and "n of m" said
|
|
11
|
+
* as it changes. Scrolling, swiping and the buttons all move it; the current
|
|
12
|
+
* slide is whichever is most in view. When every slide fits, there are no
|
|
13
|
+
* buttons.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Carousel extends AsterElement {
|
|
16
|
+
#private;
|
|
17
|
+
get slides(): readonly Slide[];
|
|
18
|
+
set slides(v: readonly Slide[]);
|
|
19
|
+
get label(): string;
|
|
20
|
+
set label(v: string);
|
|
21
|
+
/** The slide most in view. */
|
|
22
|
+
get index(): number;
|
|
23
|
+
/** Brings a slide into view (smoothly unless motion is reduced). */
|
|
24
|
+
show(index: number): void;
|
|
25
|
+
disconnectedCallback(): void;
|
|
26
|
+
protected render(): View;
|
|
27
|
+
protected updated(): void;
|
|
28
|
+
}
|