@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,290 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { AsterElement, define, uid } from "../base/element.js";
3
+ import { controlFor } from "../form/controls.js";
4
+ import { formatValue } from "../field/values.js";
5
+ import { TreeChoices } from "../picker/tree-choices.js";
6
+ import "../picker/tree-picker.js";
7
+ import { manyView } from "../select/many.js";
8
+ import "../select/select.js";
9
+ import { opLabel } from "./operators.js";
10
+ const GROUPS = ["and", "or", "not"];
11
+ /** Operators that need no value. */
12
+ const UNARY = ["set", "unset"];
13
+ /**
14
+ * Edits a rule as a tree of clauses: groups (all of, any of, none of)
15
+ * holding conditions (a field, an operator, a value) and further groups.
16
+ * The clauses are rows of a list edit, so every input shows pending, a
17
+ * changed row offers Revert, a removed one is struck through with Undo, and
18
+ * the whole rule saves with its record.
19
+ *
20
+ * Rows have `op`, `parentClauseId`, `position`, `category`, `fieldRef`,
21
+ * `value` and, on a message's top group, `message`. A value for `in` is a
22
+ * JSON array. With `messages`, each top-level group is a message shown when
23
+ * its conditions hold; otherwise there's at most one, and none means
24
+ * "always".
25
+ */
26
+ export class RuleEditor extends AsterElement {
27
+ #list = this.prop(undefined);
28
+ #category = this.prop("");
29
+ #label = this.prop("");
30
+ #messages = this.prop(false);
31
+ #fields = this.prop(undefined);
32
+ #describe = this.prop(undefined);
33
+ #readOnly = this.prop(undefined);
34
+ #empty = this.prop("Always");
35
+ #id = uid("aster-rule");
36
+ #focus;
37
+ get list() {
38
+ return this.#list.value;
39
+ }
40
+ set list(v) {
41
+ this.#list.value = v;
42
+ }
43
+ /** The rows' `category`: this editor shows and adds only these. */
44
+ get category() {
45
+ return this.#category.value;
46
+ }
47
+ set category(v) {
48
+ this.#category.value = v;
49
+ }
50
+ get label() {
51
+ return this.#label.value;
52
+ }
53
+ set label(v) {
54
+ this.#label.value = v;
55
+ }
56
+ /** Each top-level group is a message with its own conditions. */
57
+ get messages() {
58
+ return this.#messages.value;
59
+ }
60
+ set messages(v) {
61
+ this.#messages.value = v;
62
+ }
63
+ /** The fields a condition can name: a list, or a tree where names repeat across groups. */
64
+ get fields() {
65
+ return this.#fields.value;
66
+ }
67
+ set fields(v) {
68
+ this.#fields.value = v;
69
+ }
70
+ /** What a named field allows: its operators and values. */
71
+ get describe() {
72
+ return this.#describe.value;
73
+ }
74
+ set describe(v) {
75
+ this.#describe.value = v;
76
+ }
77
+ /** Why the rule can't be edited now; set, it makes the editor read-only. */
78
+ get readOnlyReason() {
79
+ return this.#readOnly.value;
80
+ }
81
+ set readOnlyReason(v) {
82
+ this.#readOnly.value = v;
83
+ }
84
+ /** Said when there's no rule. */
85
+ get empty() {
86
+ return this.#empty.value;
87
+ }
88
+ set empty(v) {
89
+ this.#empty.value = v;
90
+ }
91
+ render() {
92
+ const list = this.#list.value;
93
+ if (!list)
94
+ return nothing;
95
+ const rows = list.rows.value.filter((r) => r.edit.values.value.category === this.#category.value);
96
+ const readOnly = this.#readOnly.value !== undefined;
97
+ const roots = this.#children(rows, null);
98
+ const live = roots.filter((r) => !r.removing);
99
+ const canAdd = !readOnly && (this.#messages.value || live.length === 0);
100
+ return html `<section class="aster-rule" aria-labelledby=${`${this.#id}-label`}>
101
+ <div class="aster-rule-label" id=${`${this.#id}-label`}>${this.#label.value}</div>
102
+ ${this.#readOnly.value ? html `<p class="aster-hint">${this.#readOnly.value}</p>` : nothing}
103
+ ${roots.length === 0 ? html `<p class="aster-rule-empty">${this.#empty.value}</p>` : nothing}
104
+ ${roots.map((r, i) => this.#group(rows, r, readOnly, 0, this.#messages.value ? `Message ${i + 1}` : this.#label.value))}
105
+ ${canAdd
106
+ ? html `<button type="button" class="aster-list-edit-add" @click=${() => this.#addRoot(list)}>
107
+ <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M7 2.5v9M2.5 7h9"/></svg>${this.#messages.value ? "Add a message" : "Add a condition"}
108
+ </button>`
109
+ : nothing}
110
+ </section>`;
111
+ }
112
+ #group(rows, row, readOnly, depth, name) {
113
+ const list = this.#list.peek();
114
+ const { edit } = row;
115
+ const kids = this.#children(rows, row);
116
+ const locked = readOnly || row.removing;
117
+ const headId = `${this.#id}-${row.id}`;
118
+ const isMessage = this.#messages.value && depth === 0;
119
+ const ops = GROUPS.map((value) => ({ value, label: opLabel(edit.type.field("op")?.allowedValues, value) }));
120
+ return html `<div class="aster-rule-group" role="group" aria-labelledby=${headId} data-id=${row.id}
121
+ ?data-new=${row.isNew} ?data-removing=${row.removing} ?data-pending=${row.isNew || edit.dirty.value} style=${`--depth:${depth}`}>
122
+ <div class="aster-rule-head" id=${headId}>
123
+ ${row.removing || row.isNew ? html `<span class="aster-sr-only">${row.removing ? "To be removed:" : "New:"}</span>` : nothing}
124
+ ${isMessage
125
+ ? locked
126
+ ? html `<span class="aster-rule-static">${String(edit.values.value.message ?? "")}</span>`
127
+ : controlFor(edit, "message", { labelled: false })
128
+ : nothing}
129
+ <span class="aster-rule-when">${isMessage ? "when" : depth === 0 ? "When" : ""}</span>
130
+ ${locked
131
+ ? html `<span class="aster-rule-static">${opLabel(edit.type.field("op")?.allowedValues, edit.values.value.op)}</span>`
132
+ : html `<aster-select .model=${edit} .field=${"op"} .label=${`${name}: match`} .choices=${ops}></aster-select>`}
133
+ <span class="aster-rule-of">of these</span>
134
+ ${this.#actions(row, readOnly, name, rows)}
135
+ </div>
136
+ <div class="aster-rule-body">
137
+ ${kids.map((k, i) => (GROUPS.includes(String(k.edit.values.value.op)) ? this.#group(rows, k, locked, depth + 1, `${name}, group ${i + 1}`) : this.#leaf(k, locked, `${name}, condition ${i + 1}`, rows)))}
138
+ ${locked
139
+ ? nothing
140
+ : html `<div class="aster-rule-adds">
141
+ <button type="button" class="aster-list-edit-add" @click=${() => this.#addLeaf(list, row, rows)}>
142
+ <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M7 2.5v9M2.5 7h9"/></svg>Add condition</button>
143
+ <button type="button" class="aster-list-edit-add" @click=${() => this.#addGroup(list, row, rows)}>
144
+ <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M7 2.5v9M2.5 7h9"/></svg>Add group</button>
145
+ </div>`}
146
+ </div>
147
+ </div>`;
148
+ }
149
+ #leaf(row, readOnly, name, rows) {
150
+ const { edit } = row;
151
+ const values = edit.values.value;
152
+ const field = this.#describe.value?.(values.fieldRef).value;
153
+ const op = String(values.op ?? "");
154
+ const opMeta = edit.type.field("op")?.allowedValues;
155
+ const ops = (field?.operators ?? []).map((value) => ({ value, label: opLabel(opMeta, value) }));
156
+ const locked = readOnly || row.removing;
157
+ const fieldsOptions = this.#fields.value;
158
+ const fieldControl = locked || !fieldsOptions
159
+ ? html `<span class="aster-rule-static">${this.#fieldText(row)}</span>`
160
+ : fieldsOptions instanceof TreeChoices
161
+ ? html `<aster-tree-picker .model=${edit} .field=${"fieldRef"} .label=${`${name}: field`} .options=${fieldsOptions}></aster-tree-picker>`
162
+ : html `<aster-picker .model=${edit} .field=${"fieldRef"} .label=${`${name}: field`} .options=${fieldsOptions}></aster-picker>`;
163
+ const opControl = locked || !field
164
+ ? html `<span class="aster-rule-static">${op ? opLabel(opMeta, op) : ""}</span>`
165
+ : html `<aster-select .model=${edit} .field=${"op"} .label=${`${name}: test`} .choices=${ops}></aster-select>`;
166
+ return html `<div class="aster-rule-leaf" role="group" aria-label=${row.removing ? `${name}, to be removed` : row.isNew ? `${name}, new` : name} data-id=${row.id}
167
+ ?data-new=${row.isNew} ?data-removing=${row.removing} ?data-pending=${row.isNew || edit.dirty.value}>
168
+ ${fieldControl}${opControl}
169
+ ${UNARY.includes(op) || !field ? html `<span></span>` : this.#value(row, field, op, locked, name)}
170
+ ${this.#actions(row, readOnly, name, rows)}
171
+ ${row.removeError ? html `<p class="aster-error" role="alert">${row.removeError}</p>` : nothing}
172
+ </div>`;
173
+ }
174
+ #value(row, field, op, locked, name) {
175
+ const { edit } = row;
176
+ const raw = edit.values.value.value;
177
+ const options = field.options;
178
+ if (locked) {
179
+ const shown = options
180
+ ? (op === "in" ? parseList(raw) : [String(raw ?? "")]).map((v) => options.find((o) => o.value === v)?.label ?? v).join(", ")
181
+ : formatValue(edit.type.field("value"), raw);
182
+ return html `<span class="aster-rule-static">${shown}</span>`;
183
+ }
184
+ if (options && op === "in") {
185
+ return manyView({ label: `${name}: values`, options: options.map((o) => ({ ...o, value: String(o.value) })), values: parseList(raw),
186
+ pending: edit.pending("value").value, change: (values) => edit.set("value", values.length ? JSON.stringify(values) : null) });
187
+ }
188
+ if (options)
189
+ return html `<aster-select .model=${edit} .field=${"value"} .label=${`${name}: value`} .choices=${options}></aster-select>`;
190
+ return html `<aster-field .model=${edit} .field=${"value"} .label=${`${name}: value`}
191
+ .keyboard=${field.input === "number" ? "decimal" : ""} .placeholder=${field.input === "date" ? "YYYY-MM-DD" : ""}></aster-field>`;
192
+ }
193
+ #actions(row, readOnly, name, rows) {
194
+ if (readOnly)
195
+ return nothing;
196
+ const list = this.#list.peek();
197
+ if (row.removing) {
198
+ return html `<button type="button" class="aster-list-edit-action" aria-label=${`Undo removing ${name}`}
199
+ @click=${() => [row, ...this.#descendants(rows, row)].forEach((r) => r.removing && list.restore(r.id))}>Undo</button>`;
200
+ }
201
+ return html `<span class="aster-rule-actions">
202
+ ${row.edit.dirty.value && !row.isNew
203
+ ? html `<button type="button" class="aster-list-edit-action" aria-label=${`Revert ${name}`} @click=${() => row.edit.revert()}>Revert</button>`
204
+ : nothing}
205
+ <button type="button" class="aster-list-edit-action" aria-label=${`Remove ${name}`}
206
+ @click=${() => [...this.#descendants(rows, row).reverse(), row].forEach((r) => list.remove(r.id))}>Remove</button>
207
+ </span>`;
208
+ }
209
+ #fieldText(row) {
210
+ const options = this.#fields.value;
211
+ const ref = row.edit.values.value.fieldRef;
212
+ if (!options || ref === null || ref === undefined)
213
+ return nothing;
214
+ const record = options.record(ref)?.value;
215
+ if (!record)
216
+ return html `${String(ref)}`;
217
+ const trail = options instanceof TreeChoices ? options.trail(record) : [];
218
+ return trail.length
219
+ ? html `${options.text(record)} <span class="aster-rule-trail">(in ${trail.join(" › ")})</span>`
220
+ : html `${options.text(record)}`;
221
+ }
222
+ /** A group's rows, in position order; a new row's parent is its link until saved. */
223
+ #children(rows, parent) {
224
+ const key = parent === null ? null : parent.isNew ? parent.id : JSON.parse(parent.id)[0];
225
+ return rows
226
+ .filter((r) => {
227
+ const link = r.links?.parentClauseId;
228
+ if (link !== undefined)
229
+ return parent !== null && link === parent.id;
230
+ return (r.edit.values.value.parentClauseId ?? null) === key;
231
+ })
232
+ .sort((a, b) => Number(a.edit.values.value.position ?? 0) - Number(b.edit.values.value.position ?? 0));
233
+ }
234
+ #descendants(rows, row) {
235
+ return this.#children(rows, row).flatMap((k) => [k, ...this.#descendants(rows, k)]);
236
+ }
237
+ #nextPosition(rows, parent) {
238
+ return Math.max(0, ...this.#children(rows, parent).map((r) => Number(r.edit.values.value.position ?? 0))) + 1;
239
+ }
240
+ #add(list, parent, values, rows) {
241
+ const base = { category: this.#category.peek(), position: this.#nextPosition(rows, parent), fieldRef: null, value: null, message: null };
242
+ if (parent === null)
243
+ return list.add({ ...base, parentClauseId: null, ...values }).id;
244
+ if (!parent.isNew)
245
+ return list.add({ ...base, parentClauseId: JSON.parse(parent.id)[0], ...values }).id;
246
+ return list.add({ ...base, parentClauseId: null, ...values }, { parentClauseId: parent.id }).id;
247
+ }
248
+ #addRoot(list) {
249
+ const rows = list.rows.peek().filter((r) => r.edit.values.peek().category === this.#category.peek());
250
+ const root = this.#add(list, null, { op: "and" }, rows);
251
+ const group = list.rows.peek().find((r) => r.id === root);
252
+ this.#focus = this.#add(list, group, { op: null }, list.rows.peek());
253
+ }
254
+ #addLeaf(list, parent, rows) {
255
+ this.#focus = this.#add(list, parent, { op: null }, rows);
256
+ }
257
+ #addGroup(list, parent, rows) {
258
+ const group = this.#add(list, parent, { op: "or" }, rows);
259
+ const row = list.rows.peek().find((r) => r.id === group);
260
+ this.#focus = this.#add(list, row, { op: null }, list.rows.peek());
261
+ }
262
+ updated() {
263
+ if (this.#focus !== undefined)
264
+ this.#focusAdded(this.#focus);
265
+ }
266
+ /** Focuses an added row's first control; a picker in it renders its input a frame or so later. */
267
+ #focusAdded(id, frames = 10) {
268
+ const target = this.querySelector(`[data-id="${CSS.escape(id)}"] :is(input, [role=combobox])`);
269
+ if (target) {
270
+ if (this.#focus === id)
271
+ this.#focus = undefined;
272
+ target.focus();
273
+ }
274
+ else if (frames > 0 && this.#focus === id) {
275
+ requestAnimationFrame(() => this.#focusAdded(id, frames - 1));
276
+ }
277
+ }
278
+ }
279
+ function parseList(raw) {
280
+ if (typeof raw !== "string")
281
+ return [];
282
+ try {
283
+ const v = JSON.parse(raw);
284
+ return Array.isArray(v) ? v.map(String) : [raw];
285
+ }
286
+ catch {
287
+ return [raw];
288
+ }
289
+ }
290
+ define("aster-rule-editor", RuleEditor);
@@ -0,0 +1,24 @@
1
+ import { AsterElement, type View } from "../base/element.ts";
2
+ import "./section.ts";
3
+ export interface AccordionItem {
4
+ readonly id: string;
5
+ readonly heading: string;
6
+ readonly summary?: View;
7
+ readonly actions?: View;
8
+ readonly disabled?: boolean;
9
+ readonly body?: View;
10
+ readonly create?: () => View;
11
+ }
12
+ export declare class Accordion extends AsterElement {
13
+ #private;
14
+ get items(): readonly AccordionItem[];
15
+ set items(v: readonly AccordionItem[]);
16
+ get expanded(): readonly string[];
17
+ set expanded(v: readonly string[]);
18
+ get multiple(): boolean;
19
+ set multiple(v: boolean);
20
+ get headingLevel(): 2 | 3 | 4;
21
+ set headingLevel(v: 2 | 3 | 4);
22
+ protected render(): View;
23
+ protected updated(): void;
24
+ }
@@ -0,0 +1,90 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { AsterElement, define, each } from "../base/element.js";
3
+ import "./section.js";
4
+ export class Accordion extends AsterElement {
5
+ #items = this.prop([]);
6
+ #expanded = this.prop([]);
7
+ #multiple = this.prop(false);
8
+ #level = this.prop(2);
9
+ #created = new Map();
10
+ #restoreFocus = false;
11
+ get items() { return this.#items.value; }
12
+ set items(v) {
13
+ if (new Set(v.map(i => i.id)).size !== v.length || v.some(i => !i.id || i.body !== undefined && i.create !== undefined))
14
+ throw new TypeError("Accordion items require unique IDs and either body or create");
15
+ const active = document.activeElement?.closest("[data-accordion-id]");
16
+ this.#restoreFocus = !!active && active.closest("aster-accordion") === this && !v.some(i => i.id === active.dataset.accordionId);
17
+ const ids = new Set(v.map(i => i.id));
18
+ for (const id of this.#created.keys())
19
+ if (!ids.has(id))
20
+ this.#created.delete(id);
21
+ this.#expanded.value = this.#expanded.peek().filter(id => ids.has(id));
22
+ this.#items.value = [...v];
23
+ }
24
+ get expanded() { return this.#expanded.value; }
25
+ set expanded(v) {
26
+ if (new Set(v).size !== v.length || v.some(id => !id))
27
+ throw new TypeError("Expanded IDs must be unique");
28
+ this.#expanded.value = [...v];
29
+ }
30
+ get multiple() { return this.#multiple.value; }
31
+ set multiple(v) { this.#multiple.value = v; }
32
+ get headingLevel() { return this.#level.value; }
33
+ set headingLevel(v) { this.#level.value = v; }
34
+ render() {
35
+ const expanded = this.#expanded.value, multiple = this.#multiple.value;
36
+ return html `<div class="aster-accordion" @keydown=${(e) => this.#key(e)}>
37
+ ${each(this.#items.value, i => i.id, item => {
38
+ const open = expanded.includes(item.id);
39
+ if (open && item.create && !this.#created.has(item.id))
40
+ this.#created.set(item.id, item.create());
41
+ return html `<div class="aster-accordion-item" data-accordion-id=${item.id}>
42
+ <aster-section .heading=${item.heading} .level=${this.#level.value} .open=${open}
43
+ .summary=${item.summary ?? nothing} .actions=${item.actions ?? nothing} .disabled=${!!item.disabled}
44
+ .body=${item.body ?? this.#created.get(item.id) ?? nothing}
45
+ @section-toggle=${(e) => {
46
+ if (e.target.parentElement?.dataset.accordionId !== item.id)
47
+ return;
48
+ e.stopPropagation();
49
+ this.#expanded.value = e.detail.open ? [...(multiple || e.detail.reason === "error" ? this.#expanded.peek().filter(id => id !== item.id) : []), item.id] : this.#expanded.peek().filter(id => id !== item.id);
50
+ this.emit("expanded-change", { expanded: this.#expanded.peek(), reason: e.detail.reason });
51
+ }}></aster-section>
52
+ </div>`;
53
+ })}
54
+ </div>`;
55
+ }
56
+ updated() {
57
+ if (this.#restoreFocus) {
58
+ this.#restoreFocus = false;
59
+ this.#headers()[0]?.focus();
60
+ }
61
+ }
62
+ #headers() {
63
+ return [...this.querySelectorAll(":scope > .aster-accordion > .aster-accordion-item > aster-section > .aster-section-head > button:not(:disabled)")].filter(el => el.closest("aster-accordion") === this);
64
+ }
65
+ #key(e) {
66
+ const headers = this.#headers(), index = headers.indexOf(e.target);
67
+ if (index < 0)
68
+ return;
69
+ let next;
70
+ switch (e.key) {
71
+ case "ArrowDown":
72
+ next = (index + 1) % headers.length;
73
+ break;
74
+ case "ArrowUp":
75
+ next = (index + headers.length - 1) % headers.length;
76
+ break;
77
+ case "Home":
78
+ next = 0;
79
+ break;
80
+ case "End":
81
+ next = headers.length - 1;
82
+ break;
83
+ default: return;
84
+ }
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ headers[next]?.focus();
88
+ }
89
+ }
90
+ define("aster-accordion", Accordion);
@@ -0,0 +1,30 @@
1
+ import { AsterElement, type View } from "../base/element.ts";
2
+ /**
3
+ * A titled section that collapses. The heading is a button with
4
+ * `aria-expanded`; the body stays in the DOM, inert while collapsed, and CSS
5
+ * animates its height (a grid row from 0fr to 1fr), so no layout is
6
+ * measured in JS. A collapsed section opens when an error newly appears in
7
+ * it; collapsing it again holds while that error stays.
8
+ */
9
+ export declare class Section extends AsterElement {
10
+ #private;
11
+ get heading(): string;
12
+ set heading(v: string);
13
+ get actions(): View;
14
+ set actions(v: View);
15
+ get disabled(): boolean;
16
+ set disabled(v: boolean);
17
+ get open(): boolean;
18
+ set open(v: boolean);
19
+ get body(): View;
20
+ set body(v: View);
21
+ /** The heading's level in the page outline. */
22
+ get level(): 2 | 3 | 4;
23
+ set level(v: 2 | 3 | 4);
24
+ /** Shown beside the heading while collapsed, such as "2 changes". */
25
+ get summary(): View;
26
+ set summary(v: View);
27
+ protected render(): View;
28
+ protected updated(): void;
29
+ disconnectedCallback(): void;
30
+ }
@@ -0,0 +1,99 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { AsterElement, define, uid } from "../base/element.js";
3
+ /**
4
+ * A titled section that collapses. The heading is a button with
5
+ * `aria-expanded`; the body stays in the DOM, inert while collapsed, and CSS
6
+ * animates its height (a grid row from 0fr to 1fr), so no layout is
7
+ * measured in JS. A collapsed section opens when an error newly appears in
8
+ * it; collapsing it again holds while that error stays.
9
+ */
10
+ export class Section extends AsterElement {
11
+ #heading = this.prop("");
12
+ #open = this.prop(true);
13
+ #body = this.prop(nothing);
14
+ #summary = this.prop(nothing);
15
+ #actions = this.prop(nothing);
16
+ #level = this.prop(2);
17
+ #disabled = this.prop(false);
18
+ #id = uid("aster-section");
19
+ /** Whether the body held an error after the last commit, so only a new one opens the section. */
20
+ #errored = false;
21
+ #errors;
22
+ get heading() {
23
+ return this.#heading.value;
24
+ }
25
+ set heading(v) {
26
+ this.#heading.value = v;
27
+ }
28
+ get actions() { return this.#actions.value; }
29
+ set actions(v) { this.#actions.value = v; }
30
+ get disabled() { return this.#disabled.value; }
31
+ set disabled(v) { this.#disabled.value = v; }
32
+ get open() {
33
+ return this.#open.value;
34
+ }
35
+ set open(v) {
36
+ if (!v && this.querySelector(".aster-section-body")?.contains(document.activeElement))
37
+ this.querySelector(".aster-section-head button")?.focus();
38
+ this.#open.value = v;
39
+ }
40
+ get body() {
41
+ return this.#body.value;
42
+ }
43
+ set body(v) {
44
+ this.#body.value = v;
45
+ }
46
+ /** The heading's level in the page outline. */
47
+ get level() {
48
+ return this.#level.value;
49
+ }
50
+ set level(v) {
51
+ this.#level.value = v;
52
+ }
53
+ /** Shown beside the heading while collapsed, such as "2 changes". */
54
+ get summary() {
55
+ return this.#summary.value;
56
+ }
57
+ set summary(v) {
58
+ this.#summary.value = v;
59
+ }
60
+ render() {
61
+ const open = this.#open.value;
62
+ this.toggleAttribute("data-open", open);
63
+ return html `<div class="aster-section-head" role="heading" aria-level=${this.#level.value}>
64
+ <button type="button" ?disabled=${this.#disabled.value} aria-expanded=${open ? "true" : "false"} aria-controls=${this.#id}
65
+ @click=${() => this.#toggle(!this.#open.peek(), "user")}>
66
+ <svg viewBox="0 0 10 10" aria-hidden="true"><path d="M3.5 2 6.5 5l-3 3"/></svg>
67
+ <span>${this.#heading.value}</span>
68
+ </button>
69
+ ${open ? nothing : html `<span class="aster-section-summary">${this.#summary.value}</span>`}
70
+ ${this.#actions.value === nothing ? nothing : html `<div class="aster-section-actions">${this.#actions.value}</div>`}
71
+ </div>
72
+ <div class="aster-section-body" id=${this.#id} role="region" aria-label=${this.#heading.value} ?inert=${!open}>
73
+ <div class="aster-section-inner">${this.#body.value}</div>
74
+ </div>`;
75
+ }
76
+ updated() {
77
+ if (!this.#errors) {
78
+ this.#errors = new MutationObserver(() => this.#checkErrors());
79
+ this.#errors.observe(this, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-invalid"] });
80
+ }
81
+ this.#checkErrors();
82
+ }
83
+ disconnectedCallback() {
84
+ this.#errors?.disconnect();
85
+ this.#errors = undefined;
86
+ super.disconnectedCallback();
87
+ }
88
+ #checkErrors() {
89
+ const errored = this.querySelector(".aster-section-body [aria-invalid='true']") !== null;
90
+ if (errored && !this.#errored && !this.#open.peek())
91
+ this.#toggle(true, "error");
92
+ this.#errored = errored;
93
+ }
94
+ #toggle(open, reason) {
95
+ this.#open.value = open;
96
+ this.emit("section-toggle", { open, reason });
97
+ }
98
+ }
99
+ define("aster-section", Section);
@@ -0,0 +1,20 @@
1
+ import type { Value } from "@getaster/data";
2
+ import { AsterElement, type View } from "../base/element.ts";
3
+ import { type Option } from "./choice.ts";
4
+ /**
5
+ * A select-only combobox not bound to a form: `options`, a `value`, and a
6
+ * `change` event with the chosen value. Filter rows and toolbars use it.
7
+ */
8
+ export declare class Choice extends AsterElement {
9
+ #private;
10
+ get options(): readonly Option[];
11
+ set options(v: readonly Option[]);
12
+ get value(): Value | undefined;
13
+ set value(v: Value | undefined);
14
+ get label(): string;
15
+ set label(v: string);
16
+ get placeholder(): string;
17
+ set placeholder(v: string);
18
+ focus(options?: FocusOptions): void;
19
+ protected render(): View;
20
+ }
@@ -0,0 +1,55 @@
1
+ import { AsterElement, define, uid } from "../base/element.js";
2
+ import { ChoiceState, choiceView } from "./choice.js";
3
+ /**
4
+ * A select-only combobox not bound to a form: `options`, a `value`, and a
5
+ * `change` event with the chosen value. Filter rows and toolbars use it.
6
+ */
7
+ export class Choice extends AsterElement {
8
+ #options = this.prop([]);
9
+ #value = this.prop(undefined);
10
+ #label = this.prop("");
11
+ #placeholder = this.prop("Choose…");
12
+ #state = new ChoiceState();
13
+ #id = uid("aster-choice");
14
+ get options() {
15
+ return this.#options.value;
16
+ }
17
+ set options(v) {
18
+ this.#options.value = v;
19
+ }
20
+ get value() {
21
+ return this.#value.value;
22
+ }
23
+ set value(v) {
24
+ this.#value.value = v;
25
+ }
26
+ get label() {
27
+ return this.#label.value;
28
+ }
29
+ set label(v) {
30
+ this.#label.value = v;
31
+ }
32
+ get placeholder() {
33
+ return this.#placeholder.value;
34
+ }
35
+ set placeholder(v) {
36
+ this.#placeholder.value = v;
37
+ }
38
+ focus(options) {
39
+ this.querySelector("[role=combobox]")?.focus(options);
40
+ }
41
+ render() {
42
+ return choiceView(this.#state, {
43
+ id: this.#id,
44
+ options: this.#options.value,
45
+ value: this.#value.value,
46
+ placeholder: this.#placeholder.value,
47
+ label: this.#label.value,
48
+ choose: (v) => {
49
+ this.#value.value = v;
50
+ this.emit("change", v);
51
+ },
52
+ });
53
+ }
54
+ }
55
+ define("aster-choice", Choice);
@@ -0,0 +1,34 @@
1
+ import type { Value } from "@getaster/data";
2
+ import { type TemplateResult } from "lit-html";
3
+ export interface Option {
4
+ readonly value: Value;
5
+ readonly label: string;
6
+ }
7
+ /** Whether a choice's list is open, and which option is active in it. */
8
+ export declare class ChoiceState {
9
+ readonly open: import("@preact/signals-core").Signal<boolean>;
10
+ readonly active: import("@preact/signals-core").Signal<number>;
11
+ }
12
+ export interface ChoiceProps {
13
+ readonly id: string;
14
+ readonly options: readonly Option[];
15
+ readonly value: Value | undefined;
16
+ /** Shown when the value isn't one of the options. */
17
+ readonly placeholder: string;
18
+ readonly readOnly?: boolean;
19
+ readonly label?: string;
20
+ readonly labelledBy?: string;
21
+ readonly required?: boolean;
22
+ readonly invalid?: boolean;
23
+ readonly describedBy?: string;
24
+ readonly choose: (value: Value) => void;
25
+ }
26
+ /**
27
+ * A select-only combobox: the value in a frame, and a list drawn like the
28
+ * picker's rather than by the platform.
29
+ *
30
+ * Keyboard: ↓/↑, Enter or Space open; in the list ↓/↑, Home and End move,
31
+ * a letter jumps to the next label starting with it, Enter or Space choose,
32
+ * Escape and Tab close. The active option is `aria-activedescendant`.
33
+ */
34
+ export declare function choiceView(state: ChoiceState, p: ChoiceProps): TemplateResult;