@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.
Files changed (163) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +6 -2
  4. package/bin/aster-theme-check.js +317 -0
  5. package/dist/base/drag.d.ts +45 -0
  6. package/dist/base/drag.js +139 -0
  7. package/dist/base/element.d.ts +51 -0
  8. package/dist/base/element.js +133 -0
  9. package/dist/breadcrumbs/breadcrumbs.d.ts +23 -0
  10. package/dist/breadcrumbs/breadcrumbs.js +51 -0
  11. package/dist/button/button.d.ts +37 -0
  12. package/dist/button/button.js +108 -0
  13. package/dist/caret/caret-group.d.ts +14 -0
  14. package/dist/caret/caret-group.js +71 -0
  15. package/dist/carousel/carousel.d.ts +28 -0
  16. package/dist/carousel/carousel.js +134 -0
  17. package/dist/component/component.d.ts +73 -0
  18. package/dist/component/component.js +190 -0
  19. package/dist/component/echarts.d.ts +46 -0
  20. package/dist/component/echarts.js +95 -0
  21. package/dist/composition/authoring.d.ts +157 -0
  22. package/dist/composition/authoring.js +291 -0
  23. package/dist/composition/builtins.d.ts +1130 -0
  24. package/dist/composition/builtins.js +318 -0
  25. package/dist/composition/command-bar.d.ts +18 -0
  26. package/dist/composition/command-bar.js +34 -0
  27. package/dist/composition/commands.d.ts +14 -0
  28. package/dist/composition/commands.js +59 -0
  29. package/dist/composition/composite.d.ts +48 -0
  30. package/dist/composition/composite.js +409 -0
  31. package/dist/composition/contracts.d.ts +63 -0
  32. package/dist/composition/contracts.js +97 -0
  33. package/dist/composition/definition.d.ts +94 -0
  34. package/dist/composition/definition.js +291 -0
  35. package/dist/composition/element.d.ts +35 -0
  36. package/dist/composition/element.js +51 -0
  37. package/dist/composition/registry.d.ts +83 -0
  38. package/dist/composition/registry.js +99 -0
  39. package/dist/composition/schema.d.ts +9 -0
  40. package/dist/composition/schema.js +139 -0
  41. package/dist/composition/sessions.d.ts +15 -0
  42. package/dist/composition/sessions.js +61 -0
  43. package/dist/dashboard/dashboard.d.ts +26 -0
  44. package/dist/dashboard/dashboard.js +296 -0
  45. package/dist/dashboard/layout.d.ts +19 -0
  46. package/dist/dashboard/layout.js +48 -0
  47. package/dist/dashboard/widget.d.ts +42 -0
  48. package/dist/dashboard/widget.js +165 -0
  49. package/dist/date/calendar.d.ts +34 -0
  50. package/dist/date/calendar.js +137 -0
  51. package/dist/date/date-field.d.ts +18 -0
  52. package/dist/date/date-field.js +81 -0
  53. package/dist/date/dates.d.ts +22 -0
  54. package/dist/date/dates.js +80 -0
  55. package/dist/dialog/dialog.d.ts +23 -0
  56. package/dist/dialog/dialog.js +70 -0
  57. package/dist/field/bound-field.d.ts +55 -0
  58. package/dist/field/bound-field.js +139 -0
  59. package/dist/field/check.d.ts +11 -0
  60. package/dist/field/check.js +26 -0
  61. package/dist/field/composite-field.d.ts +35 -0
  62. package/dist/field/composite-field.js +88 -0
  63. package/dist/field/field.d.ts +29 -0
  64. package/dist/field/field.js +75 -0
  65. package/dist/field/input.d.ts +15 -0
  66. package/dist/field/input.js +18 -0
  67. package/dist/field/number-field.d.ts +33 -0
  68. package/dist/field/number-field.js +179 -0
  69. package/dist/field/numeric.d.ts +4 -0
  70. package/dist/field/numeric.js +57 -0
  71. package/dist/field/values.d.ts +14 -0
  72. package/dist/field/values.js +60 -0
  73. package/dist/filter/filter-builder.d.ts +18 -0
  74. package/dist/filter/filter-builder.js +305 -0
  75. package/dist/form/controls.d.ts +20 -0
  76. package/dist/form/controls.js +34 -0
  77. package/dist/form/form.d.ts +46 -0
  78. package/dist/form/form.js +85 -0
  79. package/dist/format.d.ts +10 -0
  80. package/dist/format.js +57 -0
  81. package/dist/grid/filters.d.ts +16 -0
  82. package/dist/grid/filters.js +66 -0
  83. package/dist/grid/grid.d.ts +209 -0
  84. package/dist/grid/grid.js +2584 -0
  85. package/dist/grid/rows.d.ts +32 -0
  86. package/dist/grid/rows.js +89 -0
  87. package/dist/grid/view.d.ts +69 -0
  88. package/dist/grid/view.js +172 -0
  89. package/dist/index.d.ts +63 -0
  90. package/dist/index.js +61 -0
  91. package/dist/layout/stack.d.ts +112 -0
  92. package/dist/layout/stack.js +21 -0
  93. package/dist/list-edit/list-edit.d.ts +42 -0
  94. package/dist/list-edit/list-edit.js +220 -0
  95. package/dist/listbox/listbox.d.ts +45 -0
  96. package/dist/listbox/listbox.js +187 -0
  97. package/dist/menu/menu.d.ts +55 -0
  98. package/dist/menu/menu.js +286 -0
  99. package/dist/motion.d.ts +8 -0
  100. package/dist/motion.js +23 -0
  101. package/dist/overlay/anchor.d.ts +18 -0
  102. package/dist/overlay/anchor.js +41 -0
  103. package/dist/picker/options.d.ts +59 -0
  104. package/dist/picker/options.js +97 -0
  105. package/dist/picker/picker.d.ts +46 -0
  106. package/dist/picker/picker.js +492 -0
  107. package/dist/picker/tree-choices.d.ts +38 -0
  108. package/dist/picker/tree-choices.js +72 -0
  109. package/dist/picker/tree-picker.d.ts +29 -0
  110. package/dist/picker/tree-picker.js +282 -0
  111. package/dist/progress/policy.d.ts +5 -0
  112. package/dist/progress/policy.js +28 -0
  113. package/dist/progress/status.d.ts +20 -0
  114. package/dist/progress/status.js +38 -0
  115. package/dist/rule/operators.d.ts +8 -0
  116. package/dist/rule/operators.js +9 -0
  117. package/dist/rule/rule-editor.d.ts +57 -0
  118. package/dist/rule/rule-editor.js +290 -0
  119. package/dist/section/accordion.d.ts +24 -0
  120. package/dist/section/accordion.js +90 -0
  121. package/dist/section/section.d.ts +30 -0
  122. package/dist/section/section.js +99 -0
  123. package/dist/select/choice-element.d.ts +20 -0
  124. package/dist/select/choice-element.js +55 -0
  125. package/dist/select/choice.d.ts +34 -0
  126. package/dist/select/choice.js +122 -0
  127. package/dist/select/many.d.ts +13 -0
  128. package/dist/select/many.js +20 -0
  129. package/dist/select/select.d.ts +16 -0
  130. package/dist/select/select.js +40 -0
  131. package/dist/sidebar/sidebar.d.ts +47 -0
  132. package/dist/sidebar/sidebar.js +105 -0
  133. package/dist/slicer/slicer.d.ts +26 -0
  134. package/dist/slicer/slicer.js +140 -0
  135. package/dist/split/split.d.ts +41 -0
  136. package/dist/split/split.js +237 -0
  137. package/dist/styles/contract.d.ts +36 -0
  138. package/dist/styles/contract.js +39 -0
  139. package/dist/switch/switch.d.ts +22 -0
  140. package/dist/switch/switch.js +69 -0
  141. package/dist/tabs/tabs.d.ts +30 -0
  142. package/dist/tabs/tabs.js +202 -0
  143. package/dist/tile-grid/scroll.d.ts +9 -0
  144. package/dist/tile-grid/scroll.js +21 -0
  145. package/dist/tile-grid/tile-grid.d.ts +64 -0
  146. package/dist/tile-grid/tile-grid.js +734 -0
  147. package/dist/toast/toast.d.ts +25 -0
  148. package/dist/toast/toast.js +92 -0
  149. package/dist/tree/navigator.d.ts +41 -0
  150. package/dist/tree/navigator.js +228 -0
  151. package/dist/window/chrome.d.ts +3 -0
  152. package/dist/window/chrome.js +7 -0
  153. package/dist/window/window.d.ts +46 -0
  154. package/dist/window/window.js +347 -0
  155. package/dist/window/workspace.d.ts +29 -0
  156. package/dist/window/workspace.js +137 -0
  157. package/package.json +45 -4
  158. package/styles/aster-ui.css +1145 -0
  159. package/styles/base.css +86 -0
  160. package/styles/contract.ts +41 -0
  161. package/styles/elements.css +1058 -0
  162. package/styles/harbour.css +126 -0
  163. package/styles/lamplight.css +118 -0
@@ -0,0 +1,347 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { AsterElement, define, uid } from "../base/element.js";
3
+ import { openWhen } from "../overlay/anchor.js";
4
+ import { windowBar } from "./chrome.js";
5
+ const STEP = 16;
6
+ const BIG_STEP = 64;
7
+ const MARGIN = 8;
8
+ /** Non-modal chrome; content ownership stays with the host page. */
9
+ export class Window extends AsterElement {
10
+ #content = this.prop(nothing);
11
+ #heading = this.prop("");
12
+ #open = this.prop(false);
13
+ #state = this.prop("normal");
14
+ #managed = this.prop(false);
15
+ #active = this.prop(false);
16
+ #preserve = this.prop(false);
17
+ #closing = this.prop(false);
18
+ #id = uid("aster-window");
19
+ #opener = null;
20
+ #at;
21
+ #bounds;
22
+ #restoreMaximized = false;
23
+ #mounted = false;
24
+ #resize;
25
+ #observed = null;
26
+ #focus = null;
27
+ #closeTask;
28
+ #generation = 0;
29
+ #beforeClose;
30
+ #dragEnd;
31
+ get heading() { return this.#heading.value; }
32
+ set heading(v) { this.#heading.value = v; }
33
+ get content() { return this.#content.value; }
34
+ set content(v) { this.#content.value = v; }
35
+ get open() { return this.#open.value; }
36
+ get state() { return this.#state.value; }
37
+ get active() { return this.#active.value; }
38
+ get managed() { return this.#managed.value; }
39
+ set managed(v) { this.#managed.value = v; }
40
+ get preserveContent() { return this.#preserve.value; }
41
+ set preserveContent(v) { this.#preserve.value = v; }
42
+ get closing() { return this.#closing.value; }
43
+ get beforeClose() { return this.#beforeClose; }
44
+ set beforeClose(v) { this.#beforeClose = v; }
45
+ connectedCallback() {
46
+ super.connectedCallback();
47
+ this.#resize = new ResizeObserver(() => {
48
+ if (!this.#open.peek() || this.#state.peek() !== "normal")
49
+ return;
50
+ this.#recover();
51
+ this.#capture();
52
+ this.emit("placementchange", this.placement());
53
+ });
54
+ this.#observe();
55
+ this.listen(window, "resize", () => this.#recover());
56
+ this.onDisconnect(() => {
57
+ this.#resize?.disconnect();
58
+ this.#observed = null;
59
+ this.#generation++;
60
+ this.#closeTask = undefined;
61
+ this.#closing.value = false;
62
+ this.#dragEnd?.();
63
+ });
64
+ }
65
+ show(opener = document.activeElement) {
66
+ if (!this.#open.peek()) {
67
+ this.#opener = opener;
68
+ this.#mounted = true;
69
+ this.#open.value = true;
70
+ }
71
+ if (this.#state.peek() === "minimized")
72
+ this.#state.value = this.#restoreMaximized ? "maximized" : "normal";
73
+ requestAnimationFrame(() => {
74
+ if (!this.isConnected || !this.#open.peek())
75
+ return;
76
+ const panel = this.#panel();
77
+ if (!panel)
78
+ return;
79
+ if (this.#state.peek() === "normal")
80
+ this.#place(this.#at ?? beside(opener, panel));
81
+ this.#applySize();
82
+ this.activate();
83
+ this.emit("statechange", this.#state.peek());
84
+ });
85
+ }
86
+ /** Activates chrome without reparenting the content or recreating its models. */
87
+ activate(focus = true) {
88
+ const panel = this.#panel();
89
+ if (!panel || !this.#open.peek() || this.#state.peek() === "minimized" || document.querySelector("dialog:modal"))
90
+ return;
91
+ if (!this.#active.peek()) {
92
+ const previous = document.activeElement;
93
+ this.#active.value = true;
94
+ // Top-layer order is insertion order; z-index cannot raise a popover.
95
+ if (panel.matches(":popover-open"))
96
+ panel.hidePopover();
97
+ panel.showPopover();
98
+ if (!focus && previous instanceof HTMLElement && previous.isConnected)
99
+ previous.focus({ preventScroll: true });
100
+ this.emit("activate");
101
+ }
102
+ if (focus)
103
+ (this.#focus?.isConnected && panel.contains(this.#focus) ? this.#focus : panel).focus({ preventScroll: true });
104
+ }
105
+ deactivate() { this.#active.value = false; }
106
+ close() {
107
+ if (this.#closeTask)
108
+ return this.#closeTask;
109
+ if (!this.#open.peek())
110
+ return Promise.resolve(true);
111
+ if (!this.#beforeClose) {
112
+ this.#finishClose();
113
+ return Promise.resolve(true);
114
+ }
115
+ const generation = this.#generation;
116
+ const guard = this.#beforeClose;
117
+ this.#closing.value = true;
118
+ this.#closeTask = Promise.resolve().then(() => guard()).then(allowed => {
119
+ if (!allowed || generation !== this.#generation || !this.isConnected)
120
+ return false;
121
+ this.#finishClose();
122
+ return true;
123
+ }).catch((error) => {
124
+ if (generation === this.#generation)
125
+ this.emit("closeerror", error);
126
+ return false;
127
+ }).finally(() => {
128
+ if (generation === this.#generation) {
129
+ this.#closing.value = false;
130
+ this.#closeTask = undefined;
131
+ }
132
+ });
133
+ return this.#closeTask;
134
+ }
135
+ toggle(opener) {
136
+ if (this.#open.peek() && this.#state.peek() !== "minimized")
137
+ void this.close();
138
+ else
139
+ this.show(opener);
140
+ }
141
+ minimize() {
142
+ if (!this.#open.peek() || this.#state.peek() === "minimized")
143
+ return;
144
+ this.#capture();
145
+ this.#restoreMaximized = this.#state.peek() === "maximized";
146
+ this.#rememberFocus();
147
+ const returnFocus = this.#active.peek() || this.#panel()?.contains(document.activeElement);
148
+ this.#state.value = "minimized";
149
+ this.#active.value = false;
150
+ this.#panel()?.hidePopover();
151
+ if (returnFocus)
152
+ this.#returnFocus();
153
+ this.#changed();
154
+ }
155
+ maximize() {
156
+ if (!this.#open.peek())
157
+ return;
158
+ this.#capture();
159
+ this.#state.value = "maximized";
160
+ this.#changed();
161
+ }
162
+ restore() {
163
+ if (!this.#open.peek())
164
+ return;
165
+ const minimized = this.#state.peek() === "minimized";
166
+ this.#state.value = minimized && this.#restoreMaximized ? "maximized" : "normal";
167
+ this.#changed();
168
+ requestAnimationFrame(() => { this.#applySize(); this.activate(); });
169
+ }
170
+ placement() {
171
+ this.#capture();
172
+ return { state: this.#state.peek(), ...(this.#bounds ? { bounds: { ...this.#bounds } } : {}), restoreMaximized: this.#restoreMaximized };
173
+ }
174
+ /** Invalid persisted data is rejected atomically. CSS limits size to the viewport. */
175
+ setPlacement(value) {
176
+ if (!["normal", "maximized", "minimized"].includes(value?.state))
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;
185
+ requestAnimationFrame(() => this.#applySize());
186
+ }
187
+ updated() {
188
+ this.#observe();
189
+ if (this.#open.peek() && this.#state.peek() !== "minimized")
190
+ this.#applySize();
191
+ }
192
+ render() {
193
+ const open = this.#open.value;
194
+ const state = this.#state.value;
195
+ const preserve = this.#preserve.value;
196
+ if (!open && !(preserve && this.#mounted))
197
+ return nothing;
198
+ const heading = `${this.#id}-heading`;
199
+ const managed = this.#managed.value;
200
+ return html `<div class="aster-window" popover="manual" role="dialog" aria-modal="false" aria-labelledby=${heading}
201
+ data-state=${state} data-active=${String(this.#active.value)} aria-busy=${String(this.#closing.value)}
202
+ tabindex="-1" ${openWhen(open && state !== "minimized")}
203
+ @pointerdown=${() => this.activate(false)} @focusin=${() => { this.#rememberFocus(); this.activate(false); }}
204
+ @keydown=${(e) => {
205
+ if (e.key !== "Escape" || e.defaultPrevented)
206
+ return;
207
+ e.preventDefault();
208
+ e.stopPropagation();
209
+ void this.close();
210
+ }}>
211
+ ${windowBar(heading, this.#heading.value, html `<button type="button" class="aster-window-move" aria-label=${`Move ${this.#heading.value}`}
212
+ ?disabled=${state !== "normal"} aria-describedby=${`${this.#id}-move-help`}
213
+ @keydown=${(e) => this.#geometryKey(e, false)}>
214
+ <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
+ </button>
216
+ <span id=${`${this.#id}-move-help`} hidden>Arrow keys move the window; with Shift, further.</span>
217
+ <button type="button" aria-label=${`Resize ${this.#heading.value}`} ?disabled=${state !== "normal"}
218
+ aria-describedby=${`${this.#id}-resize-help`} @keydown=${(e) => this.#geometryKey(e, true)}>
219
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 13 13 3M7 3h6v6M3 7v6h6"/></svg>
220
+ </button>
221
+ <span id=${`${this.#id}-resize-help`} hidden>Arrow keys resize the window; with Shift, further.</span>
222
+ ${managed ? html `<button type="button" aria-label=${`Minimize ${this.#heading.value}`} @click=${() => this.minimize()}>
223
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 12h10"/></svg></button>
224
+ <button type="button" aria-label=${`${state === "maximized" ? "Restore" : "Maximize"} ${this.#heading.value}`}
225
+ @click=${() => state === "maximized" ? this.restore() : this.maximize()}>
226
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h10v10H3zM3 6h10"/></svg></button>` : nothing}
227
+ <button type="button" class="aster-window-close" aria-label=${`Close ${this.#heading.value}`} ?disabled=${this.#closing.value} @click=${() => void this.close()}>
228
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>
229
+ </button>`, (event) => this.#drag(event))}
230
+ <div class="aster-window-body">${this.#content.value}</div>
231
+ </div>`;
232
+ }
233
+ #finishClose() {
234
+ const returnFocus = this.#active.peek() || this.#panel()?.contains(document.activeElement);
235
+ this.#rememberFocus();
236
+ this.#capture();
237
+ this.#open.value = false;
238
+ this.#active.value = false;
239
+ this.#panel()?.hidePopover();
240
+ if (returnFocus)
241
+ this.#returnFocus();
242
+ this.emit("close");
243
+ }
244
+ #rememberFocus() {
245
+ if (document.activeElement instanceof HTMLElement && this.#panel()?.contains(document.activeElement))
246
+ this.#focus = document.activeElement;
247
+ }
248
+ #returnFocus() {
249
+ if (this.#opener?.isConnected)
250
+ this.#opener.focus({ preventScroll: true });
251
+ }
252
+ #changed() {
253
+ this.emit("statechange", this.#state.peek());
254
+ this.emit("placementchange", this.placement());
255
+ }
256
+ #geometryKey(e, resize) {
257
+ if (this.#state.peek() !== "normal")
258
+ return;
259
+ const step = e.shiftKey ? BIG_STEP : STEP;
260
+ const d = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] };
261
+ const by = d[e.key];
262
+ const panel = this.#panel();
263
+ if (!by || !panel)
264
+ return;
265
+ e.preventDefault();
266
+ if (resize) {
267
+ const r = panel.getBoundingClientRect();
268
+ panel.style.width = `${Math.max(240, r.width + by[0])}px`;
269
+ panel.style.height = `${Math.max(160, r.height + by[1])}px`;
270
+ this.#recover();
271
+ this.#capture();
272
+ }
273
+ else if (this.#at)
274
+ this.#place({ x: this.#at.x + by[0], y: this.#at.y + by[1] });
275
+ this.emit("placementchange", this.placement());
276
+ }
277
+ #drag(e) {
278
+ if (e.button !== 0 || this.#state.peek() !== "normal" || e.target.closest("button") || !this.#at)
279
+ return;
280
+ const bar = e.currentTarget;
281
+ this.#dragEnd?.();
282
+ const start = { x: e.clientX - this.#at.x, y: e.clientY - this.#at.y };
283
+ bar.setPointerCapture(e.pointerId);
284
+ e.preventDefault();
285
+ const move = (m) => this.#place({ x: m.clientX - start.x, y: m.clientY - start.y });
286
+ const end = () => {
287
+ bar.removeEventListener("pointermove", move);
288
+ bar.removeEventListener("pointerup", end);
289
+ bar.removeEventListener("pointercancel", end);
290
+ this.#dragEnd = undefined;
291
+ this.emit("placementchange", this.placement());
292
+ };
293
+ bar.addEventListener("pointermove", move);
294
+ bar.addEventListener("pointerup", end);
295
+ bar.addEventListener("pointercancel", end);
296
+ this.#dragEnd = end;
297
+ }
298
+ #capture() {
299
+ const panel = this.#panel();
300
+ if (!panel?.matches(":popover-open") || this.#state.peek() !== "normal" || panel.dataset.state !== "normal")
301
+ return;
302
+ const r = panel.getBoundingClientRect();
303
+ this.#bounds = { x: r.x, y: r.y, width: r.width, height: r.height };
304
+ }
305
+ #applySize() {
306
+ const panel = this.#panel();
307
+ if (!panel)
308
+ return;
309
+ if (this.#bounds) {
310
+ panel.style.width = `${this.#bounds.width}px`;
311
+ panel.style.height = `${this.#bounds.height}px`;
312
+ }
313
+ this.#recover();
314
+ }
315
+ #recover() {
316
+ if (this.#state.peek() === "normal" && this.#at && this.#panel()?.matches(":popover-open"))
317
+ this.#place(this.#at);
318
+ }
319
+ #observe() {
320
+ const panel = this.#panel();
321
+ if (panel === this.#observed || !this.#resize)
322
+ return;
323
+ this.#resize.disconnect();
324
+ this.#observed = panel;
325
+ if (panel)
326
+ this.#resize.observe(panel);
327
+ }
328
+ #place(to) {
329
+ const panel = this.#panel();
330
+ if (!panel)
331
+ return;
332
+ const x = Math.max(MARGIN, Math.min(to.x, innerWidth - panel.offsetWidth - MARGIN));
333
+ const y = Math.max(MARGIN, Math.min(to.y, innerHeight - panel.offsetHeight - MARGIN));
334
+ this.#at = { x, y };
335
+ panel.style.setProperty("--window-x", `${x}px`);
336
+ panel.style.setProperty("--window-y", `${y}px`);
337
+ }
338
+ #panel() { return this.querySelector(".aster-window"); }
339
+ }
340
+ function beside(opener, panel) {
341
+ const r = opener?.getBoundingClientRect();
342
+ if (!r)
343
+ return { x: (innerWidth - panel.offsetWidth) / 2, y: (innerHeight - panel.offsetHeight) / 2 };
344
+ const above = r.top - MARGIN - panel.offsetHeight;
345
+ return { x: r.left, y: above >= MARGIN ? above : r.bottom + MARGIN };
346
+ }
347
+ define("aster-window", Window);
@@ -0,0 +1,29 @@
1
+ import { type ReadonlySignal } from "@preact/signals-core";
2
+ import { Window, type WindowPlacement } from "./window.ts";
3
+ export interface WorkspaceEntry {
4
+ readonly id: string;
5
+ readonly window: Window;
6
+ }
7
+ export interface WorkspaceSnapshot {
8
+ version: 1;
9
+ windows: {
10
+ id: string;
11
+ open: boolean;
12
+ placement: WindowPlacement;
13
+ }[];
14
+ active: string | null;
15
+ }
16
+ /** Coordinates registered windows through their public lifecycle only. */
17
+ export declare class Workspace {
18
+ #private;
19
+ get entries(): readonly WorkspaceEntry[];
20
+ get active(): ReadonlySignal<string | null>;
21
+ register(id: string, window: Window): () => void;
22
+ show(id: string, opener?: HTMLElement | null): void;
23
+ /** Cycles visible windows; hosts choose a shortcut that suits their page. */
24
+ cycle(backwards?: boolean): void;
25
+ snapshot(): WorkspaceSnapshot;
26
+ /** Applies placement to registered windows only; never creates content or closes dirty windows. */
27
+ restore(snapshot: WorkspaceSnapshot): void;
28
+ dispose(): void;
29
+ }
@@ -0,0 +1,137 @@
1
+ import { signal } from "@preact/signals-core";
2
+ import { Window } from "./window.js";
3
+ /** Coordinates registered windows through their public lifecycle only. */
4
+ export class Workspace {
5
+ #entries = signal([]);
6
+ #active = signal(null);
7
+ #cleanup = new Map();
8
+ #restoring = false;
9
+ #generation = 0;
10
+ get entries() { return this.#entries.value; }
11
+ get active() { return this.#active; }
12
+ register(id, window) {
13
+ if (!id || this.#cleanup.has(id) || this.#entries.peek().some(entry => entry.window === window))
14
+ throw new Error("Workspace windows require unique IDs and elements");
15
+ const activate = () => {
16
+ for (const entry of this.#entries.peek())
17
+ if (entry.window !== window)
18
+ entry.window.deactivate();
19
+ this.#active.value = id;
20
+ this.#entries.value = [...this.#entries.peek().filter(entry => entry.id !== id), { id, window }];
21
+ };
22
+ const leave = () => {
23
+ if (window.open && window.state !== "minimized")
24
+ return;
25
+ if (this.#active.peek() !== id)
26
+ return;
27
+ this.#active.value = null;
28
+ if (this.#restoring)
29
+ return;
30
+ const next = [...this.#entries.peek()].reverse().find(entry => entry.window.open && entry.window.state !== "minimized");
31
+ if (next)
32
+ next.window.activate();
33
+ };
34
+ window.addEventListener("activate", activate);
35
+ window.addEventListener("close", leave);
36
+ window.addEventListener("statechange", leave);
37
+ this.#entries.value = [...this.#entries.peek(), { id, window }];
38
+ let registered = true;
39
+ const remove = () => {
40
+ if (!registered)
41
+ return;
42
+ registered = false;
43
+ window.removeEventListener("activate", activate);
44
+ window.removeEventListener("close", leave);
45
+ window.removeEventListener("statechange", leave);
46
+ window.deactivate();
47
+ this.#cleanup.delete(id);
48
+ this.#entries.value = this.#entries.peek().filter(entry => entry.id !== id);
49
+ if (this.#active.peek() === id) {
50
+ this.#active.value = null;
51
+ if (!this.#restoring)
52
+ [...this.#entries.peek()].reverse().find(entry => entry.window.open && entry.window.state !== "minimized")?.window.activate();
53
+ }
54
+ };
55
+ this.#cleanup.set(id, remove);
56
+ return remove;
57
+ }
58
+ show(id, opener) {
59
+ if (document.querySelector("dialog:modal"))
60
+ return;
61
+ this.#find(id).show(opener);
62
+ }
63
+ /** Cycles visible windows; hosts choose a shortcut that suits their page. */
64
+ cycle(backwards = false) {
65
+ if (document.querySelector("dialog:modal"))
66
+ return;
67
+ const visible = this.#entries.peek().filter(entry => entry.window.open && entry.window.state !== "minimized");
68
+ // Registration order is not stable after activation; sort IDs for predictable cycling.
69
+ visible.sort((a, b) => a.id.localeCompare(b.id));
70
+ if (!visible.length)
71
+ return;
72
+ const current = visible.findIndex(entry => entry.id === this.#active.peek());
73
+ visible[(current + (backwards ? -1 : 1) + visible.length) % visible.length].window.activate();
74
+ }
75
+ snapshot() {
76
+ return { version: 1, windows: this.#entries.peek().map(entry => ({ id: entry.id, open: entry.window.open, placement: entry.window.placement() })), active: this.#active.peek() };
77
+ }
78
+ /** Applies placement to registered windows only; never creates content or closes dirty windows. */
79
+ restore(snapshot) {
80
+ if (snapshot?.version !== 1 || !Array.isArray(snapshot.windows))
81
+ throw new TypeError("Invalid workspace snapshot");
82
+ const known = snapshot.windows.filter(saved => this.#cleanup.has(saved.id));
83
+ const ids = new Set();
84
+ for (const saved of known) {
85
+ if (ids.has(saved.id) || typeof saved.open !== "boolean")
86
+ throw new TypeError("Invalid workspace entry");
87
+ ids.add(saved.id);
88
+ const p = saved.placement;
89
+ if (!p || !["normal", "maximized", "minimized"].includes(p.state) || (p.bounds && (![p.bounds.x, p.bounds.y, p.bounds.width, p.bounds.height].every(Number.isFinite) || p.bounds.width <= 0 || p.bounds.height <= 0)))
90
+ throw new TypeError("Invalid workspace placement");
91
+ }
92
+ const generation = ++this.#generation;
93
+ this.#restoring = true;
94
+ this.#active.value = null;
95
+ for (const entry of this.#entries.peek())
96
+ entry.window.deactivate();
97
+ try {
98
+ for (const saved of known) {
99
+ const window = this.#find(saved.id);
100
+ if (saved.open)
101
+ window.show(null);
102
+ window.setPlacement(saved.placement);
103
+ }
104
+ }
105
+ finally {
106
+ this.#restoring = false;
107
+ }
108
+ requestAnimationFrame(() => {
109
+ if (generation !== this.#generation)
110
+ return;
111
+ for (const saved of known) {
112
+ const window = this.#entries.peek().find(entry => entry.id === saved.id)?.window;
113
+ if (!window)
114
+ continue;
115
+ if (window.open && window.state !== "minimized") {
116
+ window.deactivate();
117
+ window.activate(false);
118
+ }
119
+ }
120
+ const active = this.#entries.peek().find(entry => entry.id === snapshot.active && entry.window.open && entry.window.state !== "minimized");
121
+ active?.window.activate();
122
+ });
123
+ }
124
+ dispose() {
125
+ this.#generation++;
126
+ this.#restoring = true;
127
+ for (const remove of [...this.#cleanup.values()])
128
+ remove();
129
+ this.#restoring = false;
130
+ }
131
+ #find(id) {
132
+ const entry = this.#entries.peek().find(entry => entry.id === id);
133
+ if (!entry)
134
+ throw new Error(`Unknown workspace window: ${id}`);
135
+ return entry.window;
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@getaster/elements",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-preview.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./styles/*": "./styles/*"
12
+ },
13
+ "dependencies": {
14
+ "@getaster/data": "0.1.0-preview.0",
15
+ "@preact/signals-core": "1.14.4",
16
+ "lit-html": "3.3.3"
17
+ },
18
+ "bin": {
19
+ "aster-theme-check": "./bin/aster-theme-check.js"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/GetAster/aster-ui.git",
25
+ "directory": "packages/elements"
26
+ },
27
+ "types": "./dist/index.d.ts",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "LICENSE",
34
+ "NOTICE",
35
+ "README.md",
36
+ "styles",
37
+ "bin"
38
+ ],
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "sideEffects": [
43
+ "./dist/index.js",
44
+ "./dist/**/*.js",
45
+ "./styles/*.css"
46
+ ]
47
+ }