@getaster/elements 0.3.5 → 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 +1810 -308
- 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 +181 -10
- package/styles/base.css +14 -0
- package/styles/contract.ts +16 -0
- package/styles/elements.css +167 -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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Minimize and maximize stay managed-window chrome unless the host overrides them. */
|
|
2
|
+
export function resolveWindowChrome(chrome, managed) {
|
|
3
|
+
return {
|
|
4
|
+
header: chrome.header ?? true,
|
|
5
|
+
title: chrome.title ?? true,
|
|
6
|
+
close: chrome.close ?? true,
|
|
7
|
+
minimize: chrome.minimize ?? managed,
|
|
8
|
+
maximize: chrome.maximize ?? managed,
|
|
9
|
+
resizer: chrome.resizer ?? true,
|
|
10
|
+
footer: chrome.footer ?? false,
|
|
11
|
+
statusBar: chrome.statusBar ?? false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** A placement is rejected atomically; bounds are finite and positive. */
|
|
15
|
+
export function parseWindowPlacement(value) {
|
|
16
|
+
const placement = value;
|
|
17
|
+
if (!placement || !["normal", "maximized", "minimized"].includes(placement.state))
|
|
18
|
+
throw new TypeError("Invalid window state");
|
|
19
|
+
const b = placement.bounds;
|
|
20
|
+
if (b && (![b.x, b.y, b.width, b.height].every(Number.isFinite) || b.width <= 0 || b.height <= 0))
|
|
21
|
+
throw new TypeError("Invalid window bounds");
|
|
22
|
+
return {
|
|
23
|
+
state: placement.state,
|
|
24
|
+
...(b ? { bounds: { x: b.x, y: b.y, width: b.width, height: b.height } } : {}),
|
|
25
|
+
restoreMaximized: placement.restoreMaximized === true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Keeps a window's top-left inside `limits`, never computing layout of its own. */
|
|
29
|
+
export function clampWindow(at, size, limits) {
|
|
30
|
+
return {
|
|
31
|
+
x: Math.max(limits.left, Math.min(at.x, limits.right - size.width)),
|
|
32
|
+
y: Math.max(limits.top, Math.min(at.y, limits.bottom - size.height)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function centerWindow(size, limits) {
|
|
36
|
+
return {
|
|
37
|
+
x: limits.left + (limits.right - limits.left - size.width) / 2,
|
|
38
|
+
y: limits.top + (limits.bottom - limits.top - size.height) / 2,
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/window/window.d.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
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. */
|
|
2
|
+
import { type WindowChrome, type WindowPlacement, type WindowState } from "./config.ts";
|
|
3
|
+
export type { WindowBounds, WindowChrome, WindowPlacement, WindowState } from "./config.ts";
|
|
4
|
+
/** Window chrome and content; content ownership stays with the host page. */
|
|
15
5
|
export declare class Window extends AsterElement {
|
|
16
6
|
#private;
|
|
17
7
|
get heading(): string;
|
|
@@ -28,6 +18,54 @@ export declare class Window extends AsterElement {
|
|
|
28
18
|
get closing(): boolean;
|
|
29
19
|
get beforeClose(): (() => boolean | Promise<boolean>) | undefined;
|
|
30
20
|
set beforeClose(v: (() => boolean | Promise<boolean>) | undefined);
|
|
21
|
+
/** Which parts of the window chrome it draws, as [[object:WindowChrome]]. Default `{}`. */
|
|
22
|
+
get chrome(): WindowChrome;
|
|
23
|
+
set chrome(v: WindowChrome);
|
|
24
|
+
/** Sugar for `chrome.header`. Default `true`. */
|
|
25
|
+
get showHeader(): boolean;
|
|
26
|
+
set showHeader(v: boolean);
|
|
27
|
+
/** Sugar for `chrome.title`. Default `true`; a hidden title still names the window. */
|
|
28
|
+
get showTitle(): boolean;
|
|
29
|
+
set showTitle(v: boolean);
|
|
30
|
+
/** Sugar for `chrome.close`. Default `true`. */
|
|
31
|
+
get showCloseButton(): boolean;
|
|
32
|
+
set showCloseButton(v: boolean);
|
|
33
|
+
/** Sugar for `chrome.minimize`. Default: shown for managed windows. */
|
|
34
|
+
get showMinimizeButton(): boolean;
|
|
35
|
+
set showMinimizeButton(v: boolean);
|
|
36
|
+
/** Sugar for `chrome.maximize`. Default: shown for managed windows. */
|
|
37
|
+
get showMaximizeButton(): boolean;
|
|
38
|
+
set showMaximizeButton(v: boolean);
|
|
39
|
+
/** Sugar for `chrome.resizer`. Default `true`. */
|
|
40
|
+
get showResizer(): boolean;
|
|
41
|
+
set showResizer(v: boolean);
|
|
42
|
+
/** Sugar for `chrome.footer`. Default `false`. */
|
|
43
|
+
get showFooter(): boolean;
|
|
44
|
+
set showFooter(v: boolean);
|
|
45
|
+
/** Sugar for `chrome.statusBar`. Default `false`. */
|
|
46
|
+
get showStatusBar(): boolean;
|
|
47
|
+
set showStatusBar(v: boolean);
|
|
48
|
+
/** The footer's contents. Setting it also sets `chrome.footer`; `nothing` clears the flag. */
|
|
49
|
+
get footer(): View;
|
|
50
|
+
set footer(v: View);
|
|
51
|
+
/** The status bar's contents. Setting it also sets `chrome.statusBar`; `nothing` clears the flag. */
|
|
52
|
+
get status(): View;
|
|
53
|
+
set status(v: View);
|
|
54
|
+
/** Geometry used the first time the window opens, before any placement is captured. */
|
|
55
|
+
get initialPlacement(): WindowPlacement | undefined;
|
|
56
|
+
set initialPlacement(v: WindowPlacement | undefined);
|
|
57
|
+
/** Whether the window makes the rest of the page inert while open. Default `false`. */
|
|
58
|
+
get modal(): boolean;
|
|
59
|
+
set modal(v: boolean);
|
|
60
|
+
/** Whether Escape inside the window closes it. Default `true`. */
|
|
61
|
+
get dismissOnEscape(): boolean;
|
|
62
|
+
set dismissOnEscape(v: boolean);
|
|
63
|
+
/** Whether dragging keeps the window inside its parent element. Default `false`. */
|
|
64
|
+
get keepInParentRect(): boolean;
|
|
65
|
+
set keepInParentRect(v: boolean);
|
|
66
|
+
/** Whether the window centres itself the first time it opens. Default `false`. */
|
|
67
|
+
get autoCenter(): boolean;
|
|
68
|
+
set autoCenter(v: boolean);
|
|
31
69
|
connectedCallback(): void;
|
|
32
70
|
show(opener?: HTMLElement | null): void;
|
|
33
71
|
/** Activates chrome without reparenting the content or recreating its models. */
|
package/dist/window/window.js
CHANGED
|
@@ -2,10 +2,11 @@ import { html, nothing } from "lit-html";
|
|
|
2
2
|
import { AsterElement, define, uid } from "../base/element.js";
|
|
3
3
|
import { openWhen } from "../overlay/anchor.js";
|
|
4
4
|
import { windowBar } from "./chrome.js";
|
|
5
|
+
import { centerWindow, clampWindow, parseWindowPlacement, resolveWindowChrome, } from "./config.js";
|
|
5
6
|
const STEP = 16;
|
|
6
7
|
const BIG_STEP = 64;
|
|
7
8
|
const MARGIN = 8;
|
|
8
|
-
/**
|
|
9
|
+
/** Window chrome and content; content ownership stays with the host page. */
|
|
9
10
|
export class Window extends AsterElement {
|
|
10
11
|
#content = this.prop(nothing);
|
|
11
12
|
#heading = this.prop("");
|
|
@@ -15,6 +16,14 @@ export class Window extends AsterElement {
|
|
|
15
16
|
#active = this.prop(false);
|
|
16
17
|
#preserve = this.prop(false);
|
|
17
18
|
#closing = this.prop(false);
|
|
19
|
+
#chrome = this.prop({});
|
|
20
|
+
#footer = this.prop(nothing);
|
|
21
|
+
#status = this.prop(nothing);
|
|
22
|
+
#initial = this.prop(undefined);
|
|
23
|
+
#modal = this.prop(false);
|
|
24
|
+
#dismissOnEscape = this.prop(true);
|
|
25
|
+
#keepInParentRect = this.prop(false);
|
|
26
|
+
#autoCenter = this.prop(false);
|
|
18
27
|
#id = uid("aster-window");
|
|
19
28
|
#opener = null;
|
|
20
29
|
#at;
|
|
@@ -25,6 +34,10 @@ export class Window extends AsterElement {
|
|
|
25
34
|
#observed = null;
|
|
26
35
|
#focus = null;
|
|
27
36
|
#closeTask;
|
|
37
|
+
#modalQueued = false;
|
|
38
|
+
#hostCloses = 0;
|
|
39
|
+
/** Modality as captured when the window opened; changing `modal` applies on the next open. */
|
|
40
|
+
#shownModal = false;
|
|
28
41
|
#generation = 0;
|
|
29
42
|
#beforeClose;
|
|
30
43
|
#dragEnd;
|
|
@@ -42,6 +55,62 @@ export class Window extends AsterElement {
|
|
|
42
55
|
get closing() { return this.#closing.value; }
|
|
43
56
|
get beforeClose() { return this.#beforeClose; }
|
|
44
57
|
set beforeClose(v) { this.#beforeClose = v; }
|
|
58
|
+
/** Which parts of the window chrome it draws, as [[object:WindowChrome]]. Default `{}`. */
|
|
59
|
+
get chrome() { return this.#chrome.value; }
|
|
60
|
+
set chrome(v) { this.#chrome.value = { ...(v ?? {}) }; }
|
|
61
|
+
/** Sugar for `chrome.header`. Default `true`. */
|
|
62
|
+
get showHeader() { return this.#resolved().header; }
|
|
63
|
+
set showHeader(v) { this.#setChrome("header", v); }
|
|
64
|
+
/** Sugar for `chrome.title`. Default `true`; a hidden title still names the window. */
|
|
65
|
+
get showTitle() { return this.#resolved().title; }
|
|
66
|
+
set showTitle(v) { this.#setChrome("title", v); }
|
|
67
|
+
/** Sugar for `chrome.close`. Default `true`. */
|
|
68
|
+
get showCloseButton() { return this.#resolved().close; }
|
|
69
|
+
set showCloseButton(v) { this.#setChrome("close", v); }
|
|
70
|
+
/** Sugar for `chrome.minimize`. Default: shown for managed windows. */
|
|
71
|
+
get showMinimizeButton() { return this.#resolved().minimize; }
|
|
72
|
+
set showMinimizeButton(v) { this.#setChrome("minimize", v); }
|
|
73
|
+
/** Sugar for `chrome.maximize`. Default: shown for managed windows. */
|
|
74
|
+
get showMaximizeButton() { return this.#resolved().maximize; }
|
|
75
|
+
set showMaximizeButton(v) { this.#setChrome("maximize", v); }
|
|
76
|
+
/** Sugar for `chrome.resizer`. Default `true`. */
|
|
77
|
+
get showResizer() { return this.#resolved().resizer; }
|
|
78
|
+
set showResizer(v) { this.#setChrome("resizer", v); }
|
|
79
|
+
/** Sugar for `chrome.footer`. Default `false`. */
|
|
80
|
+
get showFooter() { return this.#resolved().footer; }
|
|
81
|
+
set showFooter(v) { this.#setChrome("footer", v); }
|
|
82
|
+
/** Sugar for `chrome.statusBar`. Default `false`. */
|
|
83
|
+
get showStatusBar() { return this.#resolved().statusBar; }
|
|
84
|
+
set showStatusBar(v) { this.#setChrome("statusBar", v); }
|
|
85
|
+
/** The footer's contents. Setting it also sets `chrome.footer`; `nothing` clears the flag. */
|
|
86
|
+
get footer() { return this.#footer.value; }
|
|
87
|
+
set footer(v) {
|
|
88
|
+
this.#footer.value = v;
|
|
89
|
+
this.#setChrome("footer", v !== nothing);
|
|
90
|
+
}
|
|
91
|
+
/** The status bar's contents. Setting it also sets `chrome.statusBar`; `nothing` clears the flag. */
|
|
92
|
+
get status() { return this.#status.value; }
|
|
93
|
+
set status(v) {
|
|
94
|
+
this.#status.value = v;
|
|
95
|
+
this.#setChrome("statusBar", v !== nothing);
|
|
96
|
+
}
|
|
97
|
+
/** Geometry used the first time the window opens, before any placement is captured. */
|
|
98
|
+
get initialPlacement() { return this.#initial.value; }
|
|
99
|
+
set initialPlacement(v) {
|
|
100
|
+
this.#initial.value = v === undefined ? undefined : parseWindowPlacement(v);
|
|
101
|
+
}
|
|
102
|
+
/** Whether the window makes the rest of the page inert while open. Default `false`. */
|
|
103
|
+
get modal() { return this.#modal.value; }
|
|
104
|
+
set modal(v) { this.#modal.value = v === true; }
|
|
105
|
+
/** Whether Escape inside the window closes it. Default `true`. */
|
|
106
|
+
get dismissOnEscape() { return this.#dismissOnEscape.value; }
|
|
107
|
+
set dismissOnEscape(v) { this.#dismissOnEscape.value = v !== false; }
|
|
108
|
+
/** Whether dragging keeps the window inside its parent element. Default `false`. */
|
|
109
|
+
get keepInParentRect() { return this.#keepInParentRect.value; }
|
|
110
|
+
set keepInParentRect(v) { this.#keepInParentRect.value = v === true; }
|
|
111
|
+
/** Whether the window centres itself the first time it opens. Default `false`. */
|
|
112
|
+
get autoCenter() { return this.#autoCenter.value; }
|
|
113
|
+
set autoCenter(v) { this.#autoCenter.value = v === true; }
|
|
45
114
|
connectedCallback() {
|
|
46
115
|
super.connectedCallback();
|
|
47
116
|
this.#resize = new ResizeObserver(() => {
|
|
@@ -64,8 +133,11 @@ export class Window extends AsterElement {
|
|
|
64
133
|
}
|
|
65
134
|
show(opener = document.activeElement) {
|
|
66
135
|
if (!this.#open.peek()) {
|
|
136
|
+
if (!this.#mounted)
|
|
137
|
+
this.#adoptInitial();
|
|
67
138
|
this.#opener = opener;
|
|
68
139
|
this.#mounted = true;
|
|
140
|
+
this.#shownModal = this.#modal.peek();
|
|
69
141
|
this.#open.value = true;
|
|
70
142
|
}
|
|
71
143
|
if (this.#state.peek() === "minimized")
|
|
@@ -76,8 +148,9 @@ export class Window extends AsterElement {
|
|
|
76
148
|
const panel = this.#panel();
|
|
77
149
|
if (!panel)
|
|
78
150
|
return;
|
|
79
|
-
if (this.#state.peek() === "normal")
|
|
80
|
-
this.#place(this.#at ?? beside(opener, panel));
|
|
151
|
+
if (this.#state.peek() === "normal") {
|
|
152
|
+
this.#place(this.#at ?? (this.#autoCenter.peek() ? centerWindow(sizeOf(panel), this.#limits()) : beside(opener, panel)));
|
|
153
|
+
}
|
|
81
154
|
this.#applySize();
|
|
82
155
|
this.activate();
|
|
83
156
|
this.emit("statechange", this.#state.peek());
|
|
@@ -86,15 +159,19 @@ export class Window extends AsterElement {
|
|
|
86
159
|
/** Activates chrome without reparenting the content or recreating its models. */
|
|
87
160
|
activate(focus = true) {
|
|
88
161
|
const panel = this.#panel();
|
|
89
|
-
|
|
162
|
+
const modalHost = this.#modalHost();
|
|
163
|
+
const blocked = document.querySelector("dialog:modal");
|
|
164
|
+
if (!panel || !this.#open.peek() || this.#state.peek() === "minimized" || (blocked && blocked !== modalHost))
|
|
90
165
|
return;
|
|
91
166
|
if (!this.#active.peek()) {
|
|
92
167
|
const previous = document.activeElement;
|
|
93
168
|
this.#active.value = true;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
panel.
|
|
97
|
-
|
|
169
|
+
if (!this.#shownModal) {
|
|
170
|
+
// Top-layer order is insertion order; z-index cannot raise a popover.
|
|
171
|
+
if (panel.matches(":popover-open"))
|
|
172
|
+
panel.hidePopover();
|
|
173
|
+
panel.showPopover();
|
|
174
|
+
}
|
|
98
175
|
if (!focus && previous instanceof HTMLElement && previous.isConnected)
|
|
99
176
|
previous.focus({ preventScroll: true });
|
|
100
177
|
this.emit("activate");
|
|
@@ -147,7 +224,7 @@ export class Window extends AsterElement {
|
|
|
147
224
|
const returnFocus = this.#active.peek() || this.#panel()?.contains(document.activeElement);
|
|
148
225
|
this.#state.value = "minimized";
|
|
149
226
|
this.#active.value = false;
|
|
150
|
-
this.#
|
|
227
|
+
this.#closeHost();
|
|
151
228
|
if (returnFocus)
|
|
152
229
|
this.#returnFocus();
|
|
153
230
|
this.#changed();
|
|
@@ -173,19 +250,12 @@ export class Window extends AsterElement {
|
|
|
173
250
|
}
|
|
174
251
|
/** Invalid persisted data is rejected atomically. CSS limits size to the viewport. */
|
|
175
252
|
setPlacement(value) {
|
|
176
|
-
|
|
177
|
-
throw new TypeError("Invalid window state");
|
|
178
|
-
const b = value.bounds;
|
|
179
|
-
if (b && (![b.x, b.y, b.width, b.height].every(Number.isFinite) || b.width <= 0 || b.height <= 0))
|
|
180
|
-
throw new TypeError("Invalid window bounds");
|
|
181
|
-
this.#bounds = b ? { ...b } : undefined;
|
|
182
|
-
this.#at = b ? { x: b.x, y: b.y } : undefined;
|
|
183
|
-
this.#restoreMaximized = value.restoreMaximized === true;
|
|
184
|
-
this.#state.value = value.state;
|
|
253
|
+
this.#adopt(parseWindowPlacement(value));
|
|
185
254
|
requestAnimationFrame(() => this.#applySize());
|
|
186
255
|
}
|
|
187
256
|
updated() {
|
|
188
257
|
this.#observe();
|
|
258
|
+
this.#syncModal();
|
|
189
259
|
if (this.#open.peek() && this.#state.peek() !== "minimized")
|
|
190
260
|
this.#applySize();
|
|
191
261
|
}
|
|
@@ -196,39 +266,60 @@ export class Window extends AsterElement {
|
|
|
196
266
|
if (!open && !(preserve && this.#mounted))
|
|
197
267
|
return nothing;
|
|
198
268
|
const heading = `${this.#id}-heading`;
|
|
199
|
-
const
|
|
200
|
-
|
|
269
|
+
const title = this.#heading.value;
|
|
270
|
+
const chrome = this.#resolved();
|
|
271
|
+
const modal = this.#shownModal;
|
|
272
|
+
const titleView = chrome.title ? title : html `<span class="aster-sr-only">${title}</span>`;
|
|
273
|
+
const panel = html `<div class="aster-window" popover=${modal ? nothing : "manual"} role="dialog" aria-modal=${modal ? "true" : "false"}
|
|
274
|
+
aria-labelledby=${chrome.header ? heading : nothing} aria-label=${chrome.header ? nothing : title || nothing}
|
|
201
275
|
data-state=${state} data-active=${String(this.#active.value)} aria-busy=${String(this.#closing.value)}
|
|
202
|
-
|
|
276
|
+
data-resizer=${chrome.resizer ? nothing : "false"}
|
|
277
|
+
tabindex="-1" ${modal ? nothing : openWhen(open && state !== "minimized")}
|
|
203
278
|
@pointerdown=${() => this.activate(false)} @focusin=${() => { this.#rememberFocus(); this.activate(false); }}
|
|
204
279
|
@keydown=${(e) => {
|
|
205
280
|
if (e.key !== "Escape" || e.defaultPrevented)
|
|
206
281
|
return;
|
|
207
282
|
e.preventDefault();
|
|
208
283
|
e.stopPropagation();
|
|
209
|
-
|
|
284
|
+
if (this.#dismissOnEscape.value)
|
|
285
|
+
void this.close();
|
|
210
286
|
}}>
|
|
211
|
-
${windowBar(heading,
|
|
287
|
+
${chrome.header ? windowBar(heading, titleView, html `<button type="button" class="aster-window-move" aria-label=${`Move ${title}`}
|
|
212
288
|
?disabled=${state !== "normal"} aria-describedby=${`${this.#id}-move-help`}
|
|
213
289
|
@keydown=${(e) => this.#geometryKey(e, false)}>
|
|
214
290
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.5v13M1.5 8h13M8 1.5 6 3.5M8 1.5l2 2M8 14.5l-2-2M8 14.5l2-2M1.5 8l2-2M1.5 8l2 2M14.5 8l-2-2M14.5 8l-2 2"/></svg>
|
|
215
291
|
</button>
|
|
216
292
|
<span id=${`${this.#id}-move-help`} hidden>Arrow keys move the window; with Shift, further.</span>
|
|
217
|
-
|
|
293
|
+
${chrome.resizer ? html `<button type="button" aria-label=${`Resize ${title}`} ?disabled=${state !== "normal"}
|
|
218
294
|
aria-describedby=${`${this.#id}-resize-help`} @keydown=${(e) => this.#geometryKey(e, true)}>
|
|
219
295
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 13 13 3M7 3h6v6M3 7v6h6"/></svg>
|
|
220
296
|
</button>
|
|
221
|
-
<span id=${`${this.#id}-resize-help`} hidden>Arrow keys resize the window; with Shift, further.</span
|
|
222
|
-
${
|
|
223
|
-
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 12h10"/></svg></button
|
|
224
|
-
|
|
297
|
+
<span id=${`${this.#id}-resize-help`} hidden>Arrow keys resize the window; with Shift, further.</span>` : nothing}
|
|
298
|
+
${chrome.minimize ? html `<button type="button" aria-label=${`Minimize ${title}`} @click=${() => this.minimize()}>
|
|
299
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 12h10"/></svg></button>` : nothing}
|
|
300
|
+
${chrome.maximize ? html `<button type="button" aria-label=${`${state === "maximized" ? "Restore" : "Maximize"} ${title}`}
|
|
225
301
|
@click=${() => state === "maximized" ? this.restore() : this.maximize()}>
|
|
226
302
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h10v10H3zM3 6h10"/></svg></button>` : nothing}
|
|
227
|
-
|
|
303
|
+
${chrome.close ? html `<button type="button" class="aster-window-close" aria-label=${`Close ${title}`} ?disabled=${this.#closing.value} @click=${() => void this.close()}>
|
|
228
304
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
229
|
-
</button
|
|
305
|
+
</button>` : nothing}`, (event) => this.#drag(event)) : nothing}
|
|
230
306
|
<div class="aster-window-body">${this.#content.value}</div>
|
|
307
|
+
${chrome.footer ? html `<div class="aster-window-footer">${this.#footer.value}</div>` : nothing}
|
|
308
|
+
${chrome.statusBar ? html `<div class="aster-window-status" role="status">${this.#status.value}</div>` : nothing}
|
|
231
309
|
</div>`;
|
|
310
|
+
// The wrapper is always rendered so a modality change between openings can
|
|
311
|
+
// move the live panel instead of recreating its content and drafts.
|
|
312
|
+
return html `<dialog class="aster-window-modal"
|
|
313
|
+
@cancel=${(e) => { e.preventDefault(); if (this.#dismissOnEscape.peek())
|
|
314
|
+
void this.close(); }}
|
|
315
|
+
@close=${() => {
|
|
316
|
+
if (this.#hostCloses > 0) {
|
|
317
|
+
this.#hostCloses--;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (this.#open.peek() && this.#state.peek() !== "minimized")
|
|
321
|
+
this.#finishClose();
|
|
322
|
+
}}></dialog>${panel}`;
|
|
232
323
|
}
|
|
233
324
|
#finishClose() {
|
|
234
325
|
const returnFocus = this.#active.peek() || this.#panel()?.contains(document.activeElement);
|
|
@@ -236,11 +327,26 @@ export class Window extends AsterElement {
|
|
|
236
327
|
this.#capture();
|
|
237
328
|
this.#open.value = false;
|
|
238
329
|
this.#active.value = false;
|
|
239
|
-
this.#
|
|
330
|
+
this.#closeHost();
|
|
240
331
|
if (returnFocus)
|
|
241
332
|
this.#returnFocus();
|
|
242
333
|
this.emit("close");
|
|
243
334
|
}
|
|
335
|
+
/** Closes the wrapper without treating its queued `close` event as external. */
|
|
336
|
+
#closeHost() {
|
|
337
|
+
// Only a modal window has a wrapper that closes it; the always-present
|
|
338
|
+
// dialog stays empty and closed for a non-modal window.
|
|
339
|
+
const host = this.#shownModal ? this.#modalHost() : null;
|
|
340
|
+
if (host) {
|
|
341
|
+
// A `<form method="dialog">` close leaves the host closed; its panel is already hidden with it.
|
|
342
|
+
if (host.open) {
|
|
343
|
+
this.#hostCloses++;
|
|
344
|
+
host.close();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.#panel()?.hidePopover();
|
|
349
|
+
}
|
|
244
350
|
#rememberFocus() {
|
|
245
351
|
if (document.activeElement instanceof HTMLElement && this.#panel()?.contains(document.activeElement))
|
|
246
352
|
this.#focus = document.activeElement;
|
|
@@ -297,7 +403,7 @@ export class Window extends AsterElement {
|
|
|
297
403
|
}
|
|
298
404
|
#capture() {
|
|
299
405
|
const panel = this.#panel();
|
|
300
|
-
if (!panel
|
|
406
|
+
if (!panel || !this.#shown(panel) || this.#state.peek() !== "normal" || panel.dataset.state !== "normal")
|
|
301
407
|
return;
|
|
302
408
|
const r = panel.getBoundingClientRect();
|
|
303
409
|
this.#bounds = { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
@@ -306,6 +412,16 @@ export class Window extends AsterElement {
|
|
|
306
412
|
const panel = this.#panel();
|
|
307
413
|
if (!panel)
|
|
308
414
|
return;
|
|
415
|
+
const limits = this.#limits();
|
|
416
|
+
// CSS owns the window's min/max geometry; the panel reads these intent variables.
|
|
417
|
+
if (this.#keepInParentRect.peek()) {
|
|
418
|
+
panel.style.setProperty("--window-cap-width", `${limits.right - limits.left}px`);
|
|
419
|
+
panel.style.setProperty("--window-cap-height", `${limits.bottom - limits.top}px`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
panel.style.removeProperty("--window-cap-width");
|
|
423
|
+
panel.style.removeProperty("--window-cap-height");
|
|
424
|
+
}
|
|
309
425
|
if (this.#bounds) {
|
|
310
426
|
panel.style.width = `${this.#bounds.width}px`;
|
|
311
427
|
panel.style.height = `${this.#bounds.height}px`;
|
|
@@ -313,7 +429,7 @@ export class Window extends AsterElement {
|
|
|
313
429
|
this.#recover();
|
|
314
430
|
}
|
|
315
431
|
#recover() {
|
|
316
|
-
if (this.#state.peek() === "normal" && this.#at && this.#panel()
|
|
432
|
+
if (this.#state.peek() === "normal" && this.#at && this.#shown(this.#panel()))
|
|
317
433
|
this.#place(this.#at);
|
|
318
434
|
}
|
|
319
435
|
#observe() {
|
|
@@ -329,14 +445,93 @@ export class Window extends AsterElement {
|
|
|
329
445
|
const panel = this.#panel();
|
|
330
446
|
if (!panel)
|
|
331
447
|
return;
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
panel.style.setProperty("--window-
|
|
336
|
-
|
|
448
|
+
const at = clampWindow(to, sizeOf(panel), this.#limits());
|
|
449
|
+
this.#at = at;
|
|
450
|
+
panel.style.setProperty("--window-x", `${at.x}px`);
|
|
451
|
+
panel.style.setProperty("--window-y", `${at.y}px`);
|
|
452
|
+
}
|
|
453
|
+
/** The nearest viewport edge inside the parent, when `keepInParentRect` is on. */
|
|
454
|
+
#limits() {
|
|
455
|
+
const viewport = { left: MARGIN, top: MARGIN, right: innerWidth - MARGIN, bottom: innerHeight - MARGIN };
|
|
456
|
+
if (!this.#keepInParentRect.peek())
|
|
457
|
+
return viewport;
|
|
458
|
+
const r = this.parentElement?.getBoundingClientRect();
|
|
459
|
+
if (!r || r.width <= 0 || r.height <= 0)
|
|
460
|
+
return viewport;
|
|
461
|
+
return { left: Math.max(viewport.left, r.left), top: Math.max(viewport.top, r.top), right: Math.min(viewport.right, r.right), bottom: Math.min(viewport.bottom, r.bottom) };
|
|
462
|
+
}
|
|
463
|
+
#shown(panel) {
|
|
464
|
+
if (!panel)
|
|
465
|
+
return false;
|
|
466
|
+
if (this.#shownModal)
|
|
467
|
+
return this.#modalHost()?.open === true;
|
|
468
|
+
return panel.matches(":popover-open");
|
|
469
|
+
}
|
|
470
|
+
#resolved() {
|
|
471
|
+
return resolveWindowChrome(this.#chrome.value, this.#managed.value);
|
|
472
|
+
}
|
|
473
|
+
#setChrome(key, value) {
|
|
474
|
+
if (typeof value !== "boolean")
|
|
475
|
+
throw new TypeError(`${key} must be a boolean`);
|
|
476
|
+
this.#chrome.value = { ...this.#chrome.peek(), [key]: value };
|
|
477
|
+
}
|
|
478
|
+
#adopt(placement) {
|
|
479
|
+
this.#bounds = placement.bounds ? { ...placement.bounds } : undefined;
|
|
480
|
+
this.#at = placement.bounds ? { x: placement.bounds.x, y: placement.bounds.y } : undefined;
|
|
481
|
+
this.#restoreMaximized = placement.restoreMaximized === true;
|
|
482
|
+
this.#state.value = placement.state;
|
|
483
|
+
}
|
|
484
|
+
/** A declarative first placement applies only until any placement has been captured. */
|
|
485
|
+
#adoptInitial() {
|
|
486
|
+
const initial = this.#initial.peek();
|
|
487
|
+
if (initial && this.#at === undefined && this.#bounds === undefined)
|
|
488
|
+
this.#adopt(initial);
|
|
489
|
+
}
|
|
490
|
+
#modalHost() {
|
|
491
|
+
return this.querySelector("dialog.aster-window-modal");
|
|
492
|
+
}
|
|
493
|
+
/** The wrapper follows open, minimized and modal; the panel itself never owns modality. */
|
|
494
|
+
#syncModal() {
|
|
495
|
+
const host = this.#modalHost();
|
|
496
|
+
const panel = this.#panel();
|
|
497
|
+
// Move the live panel to the wrapper the captured modality needs. Keeping
|
|
498
|
+
// the same element preserves its DOM, focus memory and native input values.
|
|
499
|
+
if (host && panel) {
|
|
500
|
+
if (this.#shownModal && panel.parentElement !== host)
|
|
501
|
+
host.append(panel);
|
|
502
|
+
else if (!this.#shownModal && panel.parentElement === host)
|
|
503
|
+
this.append(panel);
|
|
504
|
+
}
|
|
505
|
+
if (this.#modalQueued || !host)
|
|
506
|
+
return;
|
|
507
|
+
this.#modalQueued = true;
|
|
508
|
+
// After the commit: showModal() moves focus, which must not re-enter the render effect.
|
|
509
|
+
queueMicrotask(() => {
|
|
510
|
+
this.#modalQueued = false;
|
|
511
|
+
const host = this.#modalHost();
|
|
512
|
+
if (!host)
|
|
513
|
+
return;
|
|
514
|
+
const wants = this.#shownModal && this.#open.peek() && this.#state.peek() !== "minimized";
|
|
515
|
+
if (wants && !host.open) {
|
|
516
|
+
// Modals don't stack: with a foreign modal open, showModal() throws.
|
|
517
|
+
// Fall back to show(), which keeps the window usable inside that
|
|
518
|
+
// modal's subtree and inert with the rest of the page otherwise.
|
|
519
|
+
try {
|
|
520
|
+
host.showModal();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
host.show();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else if (!wants && host.open)
|
|
527
|
+
this.#closeHost();
|
|
528
|
+
});
|
|
337
529
|
}
|
|
338
530
|
#panel() { return this.querySelector(".aster-window"); }
|
|
339
531
|
}
|
|
532
|
+
function sizeOf(panel) {
|
|
533
|
+
return { width: panel.offsetWidth, height: panel.offsetHeight };
|
|
534
|
+
}
|
|
340
535
|
function beside(opener, panel) {
|
|
341
536
|
const r = opener?.getBoundingClientRect();
|
|
342
537
|
if (!r)
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CompositeDefinition, type CompositeLocation } from "./composition/definition.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Compiles a YAML composition definition into the same plain-data definition
|
|
4
|
+
* that `defineComposite` accepts, plus the file/line/column of every node.
|
|
5
|
+
*
|
|
6
|
+
* YAML carries configuration, never code: tags, anchors and aliases are
|
|
7
|
+
* rejected, so a definition can only contain JSON-compatible values. Pass the
|
|
8
|
+
* sources to `defineCompositeRegistry(...).validate(definition, sources)` to
|
|
9
|
+
* check model, component and renderer names with located diagnostics before
|
|
10
|
+
* mounting.
|
|
11
|
+
*
|
|
12
|
+
* @param text The YAML 1.2 document, at most 256 KiB.
|
|
13
|
+
* @param file The path reported in diagnostics.
|
|
14
|
+
* @returns The detached definition and a path-to-location map.
|
|
15
|
+
*/
|
|
16
|
+
export declare function compileCompositeYaml(text: string, file?: string): {
|
|
17
|
+
definition: CompositeDefinition;
|
|
18
|
+
sources: Record<string, CompositeLocation>;
|
|
19
|
+
};
|
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isAlias, isMap, isNode, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
2
|
+
import { CompositeError, defineComposite, located } from "./composition/definition.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compiles a YAML composition definition into the same plain-data definition
|
|
5
|
+
* that `defineComposite` accepts, plus the file/line/column of every node.
|
|
6
|
+
*
|
|
7
|
+
* YAML carries configuration, never code: tags, anchors and aliases are
|
|
8
|
+
* rejected, so a definition can only contain JSON-compatible values. Pass the
|
|
9
|
+
* sources to `defineCompositeRegistry(...).validate(definition, sources)` to
|
|
10
|
+
* check model, component and renderer names with located diagnostics before
|
|
11
|
+
* mounting.
|
|
12
|
+
*
|
|
13
|
+
* @param text The YAML 1.2 document, at most 256 KiB.
|
|
14
|
+
* @param file The path reported in diagnostics.
|
|
15
|
+
* @returns The detached definition and a path-to-location map.
|
|
16
|
+
*/
|
|
17
|
+
export function compileCompositeYaml(text, file = "composite.yaml") {
|
|
18
|
+
if (text.length > 262144)
|
|
19
|
+
throw new Error(`${file}: definition exceeds 256 KiB`);
|
|
20
|
+
const lines = new LineCounter();
|
|
21
|
+
const doc = parseDocument(text, { version: "1.2", schema: "core", lineCounter: lines, uniqueKeys: true, strict: true });
|
|
22
|
+
const invalid = doc.errors[0] ?? doc.warnings[0];
|
|
23
|
+
if (invalid) {
|
|
24
|
+
const position = invalid.linePos?.[0];
|
|
25
|
+
throw new Error(position ? `${file}:${position.line}:${position.col}: ${invalid.message}` : `${file}: ${invalid.message}`);
|
|
26
|
+
}
|
|
27
|
+
if (doc.directives?.yaml.version !== "1.2")
|
|
28
|
+
throw new Error(`${file}: only YAML 1.2 is supported`);
|
|
29
|
+
const sources = Object.create(null);
|
|
30
|
+
let count = 0;
|
|
31
|
+
function walk(node, path, depth) {
|
|
32
|
+
if (++count > 10000 || depth > 40)
|
|
33
|
+
throw located(new CompositeError(path, "definition exceeds node/depth limit"), sources);
|
|
34
|
+
if (!node)
|
|
35
|
+
return;
|
|
36
|
+
const { line, col } = lines.linePos(node.range?.[0] ?? 0);
|
|
37
|
+
sources[path] = { file, line, column: col };
|
|
38
|
+
if (isAlias(node) || node.tag || node.anchor)
|
|
39
|
+
throw located(new CompositeError(path, "tags, anchors and aliases are not supported"), sources);
|
|
40
|
+
if (isMap(node))
|
|
41
|
+
for (const pair of node.items) {
|
|
42
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string")
|
|
43
|
+
throw located(new CompositeError(path, "mapping keys must be strings"), sources);
|
|
44
|
+
const next = `${path}.${pair.key.value}`;
|
|
45
|
+
walk(isNode(pair.value) ? pair.value : null, next, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
else if (isSeq(node))
|
|
48
|
+
node.items.forEach((item, i) => walk(isNode(item) ? item : null, `${path}.${i}`, depth + 1));
|
|
49
|
+
}
|
|
50
|
+
walk(doc.contents, "definition", 0);
|
|
51
|
+
try {
|
|
52
|
+
return { definition: defineComposite(doc.toJS({ maxAliasCount: 0 })), sources };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw located(error, sources);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getaster/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
"import": "./dist/index.js",
|
|
9
9
|
"default": "./dist/index.js"
|
|
10
10
|
},
|
|
11
|
-
"./styles/*": "./styles/*"
|
|
11
|
+
"./styles/*": "./styles/*",
|
|
12
|
+
"./yaml": {
|
|
13
|
+
"types": "./dist/yaml.d.ts",
|
|
14
|
+
"import": "./dist/yaml.js",
|
|
15
|
+
"default": "./dist/yaml.js"
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
18
|
"dependencies": {
|
|
14
|
-
"@getaster/data": "0.
|
|
19
|
+
"@getaster/data": "0.4.0",
|
|
15
20
|
"@preact/signals-core": "1.14.4",
|
|
16
|
-
"lit-html": "3.3.3"
|
|
21
|
+
"lit-html": "3.3.3",
|
|
22
|
+
"yaml": "^2.9.1"
|
|
17
23
|
},
|
|
18
24
|
"bin": {
|
|
19
25
|
"aster-theme-check": "./bin/aster-theme-check.js"
|