@getaster/elements 0.0.0-stage → 0.1.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2587 -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 +1147 -0
  159. package/styles/base.css +88 -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,282 @@
1
+ import { signal } from "@preact/signals-core";
2
+ import { keyFromText, keyText } from "@getaster/data";
3
+ import { html, nothing } from "lit-html";
4
+ import { live } from "lit-html/directives/live.js";
5
+ import { define } from "../base/element.js";
6
+ import { BoundField } from "../field/bound-field.js";
7
+ import { anchorStyle, openWhen, placeStyle } from "../overlay/anchor.js";
8
+ import { TreeChoices } from "./tree-choices.js";
9
+ const SEARCH_DELAY = 200;
10
+ /**
11
+ * A combobox whose popup is a tree (APG editable combobox, `aria-haspopup`
12
+ * tree): for choosing one node of a hierarchy where names repeat across
13
+ * branches. The closed control shows the chosen node's name with its path
14
+ * under it. Opening reveals the chosen node; typing filters the tree to the
15
+ * matches and the folders leading to them.
16
+ *
17
+ * Keyboard: ↓/↑ open and move, Home/End go to the first and last row, → opens
18
+ * a folder or moves into it, ← closes it or moves to its parent, Enter
19
+ * chooses (or opens a folder that can't be chosen), Escape closes and
20
+ * restores the text. ←/→ move the caret instead until ↓/↑ have moved in the
21
+ * tree since the last typing. Focus stays in the input; the active row is
22
+ * its `aria-activedescendant`.
23
+ */
24
+ export class TreePicker extends BoundField {
25
+ #options = this.prop(undefined);
26
+ #placeholder = this.prop("");
27
+ #open = this.prop(false);
28
+ #query = this.prop(undefined);
29
+ #active = this.prop(undefined);
30
+ /** The typed text the tree's filter follows, after a pause. */
31
+ #search = signal("");
32
+ #tree = this.prop(undefined);
33
+ #treeFor;
34
+ #navigating = false;
35
+ #searchTimer;
36
+ get options() {
37
+ return this.#options.value;
38
+ }
39
+ set options(v) {
40
+ this.#options.value = v;
41
+ }
42
+ get placeholder() {
43
+ return this.#placeholder.value;
44
+ }
45
+ set placeholder(v) {
46
+ this.#placeholder.value = v;
47
+ }
48
+ focus(options) {
49
+ this.querySelector("input")?.focus(options);
50
+ }
51
+ disconnectedCallback() {
52
+ this.#close();
53
+ this.#tree.peek()?.dispose();
54
+ this.#tree.value = undefined;
55
+ this.#treeFor = undefined;
56
+ super.disconnectedCallback();
57
+ }
58
+ describe(_meta, value) {
59
+ const source = this.#options.peek();
60
+ const record = source?.record(value ?? null)?.value;
61
+ if (!source || !record)
62
+ return value === null || value === undefined ? "" : String(value);
63
+ const trail = source.trail(record);
64
+ return trail.length ? `${source.text(record)} in ${trail.join(", ")}` : source.text(record);
65
+ }
66
+ control(c) {
67
+ const source = this.#options.value;
68
+ const open = this.#open.value && !c.state.readOnly;
69
+ const query = this.#query.value;
70
+ const current = source?.record(c.value ?? null)?.value;
71
+ const label = current && source ? source.text(current) : c.value === null || c.value === undefined ? "" : String(c.value);
72
+ const trail = current && source ? source.trail(current) : [];
73
+ const tree = open ? this.#tree.value : undefined;
74
+ const rows = tree?.rows.value ?? [];
75
+ const activeKey = this.#active.value;
76
+ const typed = query?.trim() ?? "";
77
+ // Until a row is made active, the first match that can be chosen is.
78
+ const first = (r) => {
79
+ const e = r.key === undefined ? undefined : tree?.entity(r.key);
80
+ return e !== undefined && source.canChoose(e) && (!typed || source.matches(e, typed));
81
+ };
82
+ const active = activeKey === undefined ? rows.findIndex(first) : rows.findIndex((r) => r.key === activeKey);
83
+ const treeId = `${c.id}-tree`;
84
+ const rowId = (key) => `${c.id}-node-${encodeURIComponent(key)}`;
85
+ const trailId = `${c.id}-trail`;
86
+ const describedBy = [c.describedBy, trail.length ? trailId : ""].filter(Boolean).join(" ") || undefined;
87
+ const activeRow = active < 0 ? undefined : rows[active];
88
+ const moveTo = (i) => {
89
+ const row = rows[Math.max(0, Math.min(rows.length - 1, i))];
90
+ if (!row?.key)
91
+ return;
92
+ this.#active.value = row.key;
93
+ this.#navigating = true;
94
+ requestAnimationFrame(() => this.querySelector(`#${CSS.escape(rowId(row.key))}`)?.scrollIntoView({ block: "nearest" }));
95
+ };
96
+ const choose = (row) => {
97
+ if (!row?.key || !source || !tree)
98
+ return;
99
+ const entity = tree.entity(row.key);
100
+ if (!entity)
101
+ return;
102
+ if (!source.canChoose(entity)) {
103
+ if (row.folder !== false)
104
+ void tree.toggle(row.key);
105
+ return;
106
+ }
107
+ this.write(keyFromText(row.key)[0] ?? null);
108
+ this.#close();
109
+ };
110
+ const status = !tree
111
+ ? ""
112
+ : tree.tooBroad.value
113
+ ? "Too many matches; type more."
114
+ : tree.searching.value || this.#search.value.trim() !== typed
115
+ ? "Searching…"
116
+ : typed && rows.length === 0
117
+ ? `Nothing matches “${typed}”.`
118
+ : "";
119
+ return html `<div class="aster-picker aster-tree-picker">
120
+ <label class="aster-field" data-ring-owner style=${anchorStyle(c.id)}>
121
+ <input id=${c.id} role="combobox" .value=${live(query ?? label)}
122
+ aria-label=${this.labelled ? nothing : c.label}
123
+ placeholder=${this.#placeholder.value || (c.meta?.nullable ? "None" : "Choose…")}
124
+ aria-haspopup="tree" aria-expanded=${open ? "true" : "false"} aria-controls=${treeId} aria-autocomplete="list"
125
+ aria-activedescendant=${open && activeRow?.key ? rowId(activeRow.key) : nothing}
126
+ aria-required=${c.state.required ? "true" : nothing}
127
+ aria-invalid=${c.invalid ? "true" : nothing}
128
+ aria-describedby=${describedBy ?? nothing}
129
+ ?readonly=${c.state.readOnly}
130
+ autocomplete="off" spellcheck="false"
131
+ @input=${(e) => this.#type(e.target.value)}
132
+ @click=${(e) => {
133
+ if (this.#open.peek())
134
+ return this.#close();
135
+ this.#show();
136
+ e.target.select();
137
+ }}
138
+ @focus=${(e) => e.target.select()}
139
+ @blur=${(e) => {
140
+ if (this.contains(e.relatedTarget))
141
+ return;
142
+ this.#close();
143
+ }}
144
+ @keydown=${(e) => {
145
+ if (c.state.readOnly)
146
+ return;
147
+ const isOpen = this.#open.peek();
148
+ switch (e.key) {
149
+ case "ArrowDown":
150
+ case "ArrowUp":
151
+ e.preventDefault();
152
+ if (!isOpen)
153
+ return this.#show();
154
+ return moveTo(active < 0 ? 0 : active + (e.key === "ArrowDown" ? 1 : -1));
155
+ case "Home":
156
+ case "End":
157
+ if (!isOpen || !this.#navigating)
158
+ return;
159
+ e.preventDefault();
160
+ return moveTo(e.key === "Home" ? 0 : rows.length - 1);
161
+ case "ArrowRight":
162
+ if (!isOpen || !this.#navigating || !activeRow?.key || !tree || !activeRow.folder)
163
+ return;
164
+ e.preventDefault();
165
+ if (!activeRow.open)
166
+ return void tree.expand(activeRow.key);
167
+ return moveTo(active + 1);
168
+ case "ArrowLeft": {
169
+ if (!isOpen || !this.#navigating || !activeRow?.key || !tree)
170
+ return;
171
+ e.preventDefault();
172
+ if (activeRow.open)
173
+ return tree.collapse(activeRow.key);
174
+ const parent = rows.findIndex((r) => r.key === activeRow.parent);
175
+ return parent >= 0 ? moveTo(parent) : undefined;
176
+ }
177
+ case "Enter":
178
+ if (!isOpen)
179
+ return;
180
+ e.preventDefault();
181
+ return choose(activeRow);
182
+ case "Escape":
183
+ if (!isOpen)
184
+ return;
185
+ e.preventDefault();
186
+ e.stopPropagation();
187
+ return this.#close();
188
+ }
189
+ }}>
190
+ <svg class="aster-picker-chevron" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 3.8 5 6.8l3-3"/></svg>
191
+ </label>
192
+ ${trail.length
193
+ ? html `<p class="aster-tree-picker-trail" id=${trailId}><span class="aster-sr-only">In </span>${trail.map((name, i) => html `${i ? html `<span aria-hidden="true"> › </span><span class="aster-sr-only">, </span>` : nothing}${name}`)}</p>`
194
+ : nothing}
195
+ <div class="aster-picker-list aster-popover aster-tree-picker-list" popover="manual" ${openWhen(open)} style=${placeStyle(c.id)}
196
+ id=${treeId} role="tree" aria-label=${c.label} aria-busy=${status === "Searching…" ? "true" : nothing}>
197
+ ${rows.map((row, i) => this.#row(row, i === active, c.value, typed, rowId, choose))}
198
+ ${status || (tree && rows.length === 0) ? html `<div class="aster-picker-empty" role="none">${status || "No choices."}</div>` : nothing}
199
+ </div>
200
+ <div class="aster-sr-only" role="status">${open && status !== "Searching…" ? status : ""}</div>
201
+ </div>`;
202
+ }
203
+ #row(row, active, chosen, typed, rowId, choose) {
204
+ const source = this.#options.peek();
205
+ const tree = this.#tree.peek();
206
+ const indent = `--level: ${row.level}`;
207
+ if (!row.key) {
208
+ // A row not loaded yet: ask for its page.
209
+ queueMicrotask(() => void row.collection.ensure(row.index, row.index + row.collection.pageSize).catch(() => { }));
210
+ return html `<div class="aster-tree-option" role="none" style=${indent}><span class="aster-tree-option-twisty"></span>Loading…</div>`;
211
+ }
212
+ const key = row.key;
213
+ const entity = tree.entity(key);
214
+ const can = entity ? source.canChoose(entity) : false;
215
+ const selected = chosen !== null && chosen !== undefined && keyText([chosen]) === key;
216
+ const toggle = (e) => {
217
+ e.stopPropagation();
218
+ void tree.toggle(key);
219
+ };
220
+ return html `<div class="aster-tree-option" role="treeitem" id=${rowId(key)} style=${indent}
221
+ aria-level=${row.level} aria-posinset=${row.position} aria-setsize=${row.siblings}
222
+ aria-expanded=${row.folder === false || (row.folder === undefined && can) ? nothing : row.open ? "true" : "false"}
223
+ aria-selected=${selected ? "true" : "false"}
224
+ ?data-active=${active} ?data-group=${!can} ?data-context=${typed !== "" && entity !== undefined && !source.matches(entity, typed)}
225
+ @pointerdown=${(e) => e.preventDefault()}
226
+ @click=${() => {
227
+ this.#active.value = key;
228
+ choose(row);
229
+ }}>
230
+ <span class="aster-tree-option-twisty" @click=${row.folder === false ? nothing : toggle}>
231
+ ${row.folder === false || (row.folder === undefined && can) ? nothing : html `<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M3.8 2 6.8 5l-3 3"/></svg>`}
232
+ </span>${entity ? source.text(entity) : ""}
233
+ </div>`;
234
+ }
235
+ #ensureTree() {
236
+ const source = this.#options.peek();
237
+ if (!source)
238
+ return undefined;
239
+ if (this.#treeFor !== source) {
240
+ this.#tree.peek()?.dispose();
241
+ this.#tree.value = source.tree(this.#search);
242
+ this.#treeFor = source;
243
+ }
244
+ return this.#tree.peek();
245
+ }
246
+ #show() {
247
+ if (this.#open.peek())
248
+ return;
249
+ const tree = this.#ensureTree();
250
+ if (!tree)
251
+ return;
252
+ this.#open.value = true;
253
+ this.#navigating = false;
254
+ const value = this.model?.values.peek()[this.field];
255
+ if (value === null || value === undefined) {
256
+ this.#active.value = undefined;
257
+ return;
258
+ }
259
+ const key = keyText([value]);
260
+ this.#active.value = key;
261
+ void tree.reveal(key).then(() => {
262
+ if (this.#active.peek() === key)
263
+ this.querySelector(`[aria-selected="true"]`)?.scrollIntoView({ block: "nearest" });
264
+ });
265
+ }
266
+ #type(text) {
267
+ this.#query.value = text;
268
+ this.#active.value = undefined;
269
+ this.#navigating = false;
270
+ this.#show();
271
+ clearTimeout(this.#searchTimer);
272
+ this.#searchTimer = setTimeout(() => (this.#search.value = text.trim()), SEARCH_DELAY);
273
+ }
274
+ #close() {
275
+ clearTimeout(this.#searchTimer);
276
+ this.#open.value = false;
277
+ this.#query.value = undefined;
278
+ this.#search.value = "";
279
+ this.#navigating = false;
280
+ }
281
+ }
282
+ define("aster-tree-picker", TreePicker);
@@ -0,0 +1,5 @@
1
+ import { type ProgressPolicy } from "@getaster/data";
2
+ /** Configure application/region defaults; mounted descendants follow changes. */
3
+ export declare function provideProgressPolicy(root: Element, options: ProgressPolicy): void;
4
+ /** Merge application/region providers, including shadow hosts, from outer to inner. */
5
+ export declare function progressPolicyOf(element: Element): Readonly<Required<ProgressPolicy>>;
@@ -0,0 +1,28 @@
1
+ import { batch, signal } from "@preact/signals-core";
2
+ import { DEFAULT_PROGRESS_POLICY, progressPolicy } from "@getaster/data";
3
+ const policies = new WeakMap();
4
+ const revision = signal(0);
5
+ /** Configure application/region defaults; mounted descendants follow changes. */
6
+ export function provideProgressPolicy(root, options) {
7
+ progressPolicy(options);
8
+ const snapshot = Object.freeze({ ...options });
9
+ batch(() => {
10
+ const existing = policies.get(root);
11
+ if (existing)
12
+ existing.value = snapshot;
13
+ else
14
+ policies.set(root, signal(snapshot));
15
+ revision.value++;
16
+ });
17
+ }
18
+ /** Merge application/region providers, including shadow hosts, from outer to inner. */
19
+ export function progressPolicyOf(element) {
20
+ revision.value;
21
+ const chain = [];
22
+ for (let node = element; node; node = node.parentNode ?? (node instanceof ShadowRoot ? node.host : null)) {
23
+ const policy = node instanceof Element ? policies.get(node)?.value : undefined;
24
+ if (policy)
25
+ chain.push(policy);
26
+ }
27
+ return Object.freeze(Object.assign({}, DEFAULT_PROGRESS_POLICY, ...chain.reverse()));
28
+ }
@@ -0,0 +1,20 @@
1
+ import { type ProgressPolicy } from "@getaster/data";
2
+ import { AsterElement, type View } from "../base/element.ts";
3
+ /** A reserved live-status line; request state and action availability are never delayed. */
4
+ export declare class ProgressStatus extends AsterElement {
5
+ #private;
6
+ get busy(): boolean;
7
+ set busy(value: boolean);
8
+ get error(): string | undefined;
9
+ set error(value: string | undefined);
10
+ get progressPolicy(): ProgressPolicy;
11
+ set progressPolicy(value: ProgressPolicy);
12
+ get pendingText(): string;
13
+ set pendingText(value: string);
14
+ get completeText(): string;
15
+ set completeText(value: string);
16
+ get reserveSpace(): boolean;
17
+ set reserveSpace(value: boolean);
18
+ connectedCallback(): void;
19
+ protected render(): View;
20
+ }
@@ -0,0 +1,38 @@
1
+ import { ProgressState, progressPolicy } from "@getaster/data";
2
+ import { html, nothing } from "lit-html";
3
+ import { AsterElement, define } from "../base/element.js";
4
+ import { progressPolicyOf } from "./policy.js";
5
+ /** A reserved live-status line; request state and action availability are never delayed. */
6
+ export class ProgressStatus extends AsterElement {
7
+ #busy = this.prop(false);
8
+ #error = this.prop(undefined);
9
+ #policy = this.prop({});
10
+ #pending = this.prop("Working…");
11
+ #complete = this.prop("Updated");
12
+ #reserve = this.prop(true);
13
+ #state;
14
+ get busy() { return this.#busy.value; }
15
+ set busy(value) { this.#busy.value = value; }
16
+ get error() { return this.#error.value; }
17
+ set error(value) { this.#error.value = value; }
18
+ get progressPolicy() { return this.#policy.value; }
19
+ set progressPolicy(value) { progressPolicy(value); this.#policy.value = Object.freeze({ ...value }); }
20
+ get pendingText() { return this.#pending.value; }
21
+ set pendingText(value) { this.#pending.value = value; }
22
+ get completeText() { return this.#complete.value; }
23
+ set completeText(value) { this.#complete.value = value; }
24
+ get reserveSpace() { return this.#reserve.value; }
25
+ set reserveSpace(value) { this.#reserve.value = value; }
26
+ connectedCallback() {
27
+ this.#state = new ProgressState(() => ({ busy: this.#busy.value, error: this.#error.value }), () => ({ ...progressPolicyOf(this), ...this.#policy.value }));
28
+ this.onDisconnect(() => { this.#state?.dispose(); this.#state = undefined; });
29
+ super.connectedCallback();
30
+ }
31
+ render() {
32
+ const phase = this.#state?.phase.value ?? "idle";
33
+ const text = phase === "working" ? this.#pending.value : phase === "complete" ? this.#complete.value : phase === "error" ? this.#error.value || "Could not complete." : "";
34
+ return html `<span class="aster-progress-status" role="status" aria-atomic="true"
35
+ ?data-reserve=${this.#reserve.value} data-phase=${phase}>${text ?? nothing}</span>`;
36
+ }
37
+ }
38
+ define("aster-progress-status", ProgressStatus);
@@ -0,0 +1,8 @@
1
+ import type { Value } from "@getaster/data";
2
+ /** Metadata labels used by persisted rule clauses. */
3
+ export declare function opLabel(allowed: readonly {
4
+ readonly value: Value;
5
+ readonly label?: string;
6
+ }[] | undefined, op: Value | undefined): string;
7
+ /** Human labels shared by general query condition controls. */
8
+ export declare function filterOperatorLabel(op: string): string;
@@ -0,0 +1,9 @@
1
+ /** Metadata labels used by persisted rule clauses. */
2
+ export function opLabel(allowed, op) {
3
+ return allowed?.find((a) => a.value === op)?.label ?? String(op ?? "");
4
+ }
5
+ /** Human labels shared by general query condition controls. */
6
+ export function filterOperatorLabel(op) {
7
+ const labels = { eq: "equals", ne: "does not equal", gt: "greater than", ge: "at least", lt: "less than", le: "at most", in: "is one of", isnull: "is null", notnull: "is not null", contains: "contains", startswith: "starts with", endswith: "ends with", and: "All", or: "Any", not: "Not all", any: "Any related record", all: "Every related record", via: "Related record" };
8
+ return labels[op] ?? op;
9
+ }
@@ -0,0 +1,57 @@
1
+ import type { ListEdit, Value } from "@getaster/data";
2
+ import type { ReadonlySignal } from "@preact/signals-core";
3
+ import { AsterElement, type View } from "../base/element.ts";
4
+ import type { PickerOptions } from "../picker/options.ts";
5
+ import { TreeChoices } from "../picker/tree-choices.ts";
6
+ import "../picker/tree-picker.ts";
7
+ import type { Option } from "../select/choice.ts";
8
+ import "../select/select.ts";
9
+ /** What a condition can test about the field it names. */
10
+ export interface RuleField {
11
+ /** The operators that suit the field, from the clause's operator values. */
12
+ readonly operators: readonly string[];
13
+ /** The value's control: typed text, a date, or one of `options`. */
14
+ readonly input: "text" | "number" | "date";
15
+ /** Values to choose from; with `in`, several. */
16
+ readonly options?: readonly Option[];
17
+ }
18
+ /**
19
+ * Edits a rule as a tree of clauses: groups (all of, any of, none of)
20
+ * holding conditions (a field, an operator, a value) and further groups.
21
+ * The clauses are rows of a list edit, so every input shows pending, a
22
+ * changed row offers Revert, a removed one is struck through with Undo, and
23
+ * the whole rule saves with its record.
24
+ *
25
+ * Rows have `op`, `parentClauseId`, `position`, `category`, `fieldRef`,
26
+ * `value` and, on a message's top group, `message`. A value for `in` is a
27
+ * JSON array. With `messages`, each top-level group is a message shown when
28
+ * its conditions hold; otherwise there's at most one, and none means
29
+ * "always".
30
+ */
31
+ export declare class RuleEditor extends AsterElement {
32
+ #private;
33
+ get list(): ListEdit | undefined;
34
+ set list(v: ListEdit | undefined);
35
+ /** The rows' `category`: this editor shows and adds only these. */
36
+ get category(): string;
37
+ set category(v: string);
38
+ get label(): string;
39
+ set label(v: string);
40
+ /** Each top-level group is a message with its own conditions. */
41
+ get messages(): boolean;
42
+ set messages(v: boolean);
43
+ /** The fields a condition can name: a list, or a tree where names repeat across groups. */
44
+ get fields(): PickerOptions | TreeChoices | undefined;
45
+ set fields(v: PickerOptions | TreeChoices | undefined);
46
+ /** What a named field allows: its operators and values. */
47
+ get describe(): ((fieldRef: Value | undefined) => ReadonlySignal<RuleField | undefined>) | undefined;
48
+ set describe(v: ((fieldRef: Value | undefined) => ReadonlySignal<RuleField | undefined>) | undefined);
49
+ /** Why the rule can't be edited now; set, it makes the editor read-only. */
50
+ get readOnlyReason(): string | undefined;
51
+ set readOnlyReason(v: string | undefined);
52
+ /** Said when there's no rule. */
53
+ get empty(): string;
54
+ set empty(v: string);
55
+ protected render(): View;
56
+ protected updated(): void;
57
+ }