@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,55 @@
1
+ import type { EditModel, Field as FieldMeta, FieldState, Value } from "@getaster/data";
2
+ import { AsterElement, type View } from "../base/element.ts";
3
+ /** What a control needs to render itself inside a bound field. */
4
+ export interface ControlContext {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly meta: FieldMeta | undefined;
8
+ readonly value: Value | undefined;
9
+ readonly state: FieldState;
10
+ /** `aria-describedby` for the control: the error, then the read-only reason. */
11
+ readonly describedBy: string | undefined;
12
+ readonly invalid: boolean;
13
+ readonly label: string;
14
+ }
15
+ /**
16
+ * A control bound to one field of an edit model, with what every field
17
+ * shows: its label and required marker, pending styling and a revert, the
18
+ * read-only reason, and the error under it.
19
+ *
20
+ * The field's state (from metadata and page rules) decides: a hidden field
21
+ * renders nothing and a read-only one shows its reason. Most fields are
22
+ * required, so the label marks the optional ones; the control carries
23
+ * `aria-required` either way.
24
+ * Pending is the model's: the value differs from the baseline, or it's forced.
25
+ * A value the server hides on this record shows as a read-only box saying
26
+ * "Hidden", whatever the control, with the reason as its description.
27
+ */
28
+ export declare abstract class BoundField extends AsterElement {
29
+ #private;
30
+ get model(): EditModel | undefined;
31
+ set model(v: EditModel | undefined);
32
+ get field(): string;
33
+ set field(v: string);
34
+ /** The label; defaults to the field's title from metadata. */
35
+ get label(): string;
36
+ set label(v: string);
37
+ /** Show the label above the control. Otherwise it's the accessible name only. */
38
+ get labelled(): boolean;
39
+ set labelled(v: boolean);
40
+ /**
41
+ * Reasons shown once for several fields, by the id of the element that
42
+ * shows them; such a reason isn't repeated here, only referenced.
43
+ */
44
+ get sharedReasons(): ReadonlyMap<string, string>;
45
+ set sharedReasons(v: ReadonlyMap<string, string>);
46
+ protected abstract control(c: ControlContext): View;
47
+ /** Control-specific validation supplements the model's errors. */
48
+ protected validation(_meta: FieldMeta | undefined, _value: Value | undefined): string | undefined;
49
+ /** The frame and anything beside it; a subclass can wrap the control. */
50
+ protected row(c: ControlContext): View;
51
+ /** A value as words, for the revert's label; a picker names the record. */
52
+ protected describe(meta: FieldMeta | undefined, value: Value | undefined): string;
53
+ protected write(value: Value): void;
54
+ protected render(): View;
55
+ }
@@ -0,0 +1,139 @@
1
+ import { DEFAULT_STATE } from "@getaster/data";
2
+ import { html, nothing } from "lit-html";
3
+ import { AsterElement, uid } from "../base/element.js";
4
+ import { formatValue } from "./values.js";
5
+ /**
6
+ * A control bound to one field of an edit model, with what every field
7
+ * shows: its label and required marker, pending styling and a revert, the
8
+ * read-only reason, and the error under it.
9
+ *
10
+ * The field's state (from metadata and page rules) decides: a hidden field
11
+ * renders nothing and a read-only one shows its reason. Most fields are
12
+ * required, so the label marks the optional ones; the control carries
13
+ * `aria-required` either way.
14
+ * Pending is the model's: the value differs from the baseline, or it's forced.
15
+ * A value the server hides on this record shows as a read-only box saying
16
+ * "Hidden", whatever the control, with the reason as its description.
17
+ */
18
+ export class BoundField extends AsterElement {
19
+ #model = this.prop(undefined);
20
+ #field = this.prop("");
21
+ #label = this.prop("");
22
+ #labelled = this.prop(false);
23
+ #shared = this.prop(new Map());
24
+ #uid = uid("aster-field");
25
+ get model() {
26
+ return this.#model.value;
27
+ }
28
+ set model(v) {
29
+ this.#model.value = v;
30
+ }
31
+ get field() {
32
+ return this.#field.value;
33
+ }
34
+ set field(v) {
35
+ this.#field.value = v;
36
+ }
37
+ /** The label; defaults to the field's title from metadata. */
38
+ get label() {
39
+ return this.#label.value;
40
+ }
41
+ set label(v) {
42
+ this.#label.value = v;
43
+ }
44
+ /** Show the label above the control. Otherwise it's the accessible name only. */
45
+ get labelled() {
46
+ return this.#labelled.value;
47
+ }
48
+ set labelled(v) {
49
+ this.#labelled.value = v;
50
+ }
51
+ /**
52
+ * Reasons shown once for several fields, by the id of the element that
53
+ * shows them; such a reason isn't repeated here, only referenced.
54
+ */
55
+ get sharedReasons() {
56
+ return this.#shared.value;
57
+ }
58
+ set sharedReasons(v) {
59
+ this.#shared.value = v;
60
+ }
61
+ /** Control-specific validation supplements the model's errors. */
62
+ validation(_meta, _value) { return undefined; }
63
+ /** The frame and anything beside it; a subclass can wrap the control. */
64
+ row(c) {
65
+ return this.control(c);
66
+ }
67
+ /** A value as words, for the revert's label; a picker names the record. */
68
+ describe(meta, value) {
69
+ return formatValue(meta, value);
70
+ }
71
+ #hiddenValue(c) {
72
+ return html `<label class="aster-field aster-field-hidden" data-ring-owner>
73
+ <input id=${c.id} type="text" readonly .value=${""} placeholder="Hidden"
74
+ aria-label=${this.#labelled.value ? nothing : c.label}
75
+ aria-describedby=${c.describedBy ?? nothing}>
76
+ </label>`;
77
+ }
78
+ write(value) {
79
+ this.#model.peek()?.set(this.#field.peek(), value);
80
+ }
81
+ render() {
82
+ const model = this.#model.value;
83
+ const name = this.#field.value;
84
+ const meta = model?.type.field(name);
85
+ const state = model && meta ? model.state(name).value : DEFAULT_STATE;
86
+ this.hidden = !state.visible;
87
+ if (!state.visible)
88
+ return nothing;
89
+ const hidden = model?.hidden.value.has(name) ?? false;
90
+ const error = model?.errors.value[name]?.[0] ?? this.validation(meta, model?.values.value[name]);
91
+ const pending = model && meta ? model.pending(name).value : false;
92
+ const reason = state.readOnly ? state.reasons.readOnly : undefined;
93
+ const sharedId = reason === undefined ? undefined : this.#shared.value.get(reason);
94
+ const errorId = `${this.#uid}-error`;
95
+ const reasonId = sharedId ?? `${this.#uid}-reason`;
96
+ const pendingId = `${this.#uid}-pending`;
97
+ const suggestedId = `${this.#uid}-suggested`;
98
+ const suggested = !hidden && !state.readOnly && (model?.derived(name).value ?? false);
99
+ // A new record's fields aren't "changed"; a new list row says it's new in its first cell. A replay says what its change did.
100
+ const replay = model?.replaying ?? false;
101
+ const changed = pending && (replay || !model?.isNew.value);
102
+ const label = this.#label.value || meta?.title || name;
103
+ const c = {
104
+ id: this.#uid,
105
+ name,
106
+ meta,
107
+ value: model?.values.value[name],
108
+ state,
109
+ describedBy: [error ? errorId : "", reason ? reasonId : "", changed ? pendingId : "", suggested ? suggestedId : ""].filter(Boolean).join(" ") || undefined,
110
+ invalid: error !== undefined,
111
+ label,
112
+ };
113
+ this.toggleAttribute("data-pending", pending);
114
+ this.toggleAttribute("data-readonly", state.readOnly);
115
+ this.toggleAttribute("data-value-hidden", hidden);
116
+ this.toggleAttribute("data-suggested", suggested);
117
+ const was = this.describe(meta, model?.baseline.value[name]) || "empty";
118
+ // Pending is drawn as a dot and an edge; this says it in words.
119
+ const added = replay && (model?.baseline.value[name] ?? null) === null && model?.values.value[name] !== undefined && Object.keys(model.baseline.value).length === 0;
120
+ const pendingText = !replay ? `Changed, not saved yet; was ${was}` : added ? "Added in this change" : `Changed in this change; was ${was}`;
121
+ const revert = pending && !state.readOnly && !model?.isNew.value
122
+ ? html `<button type="button" class="aster-revert" title=${`Was ${was}`}
123
+ aria-label=${`Revert ${label} to ${was}`}
124
+ @click=${() => model?.revert(name)}>Revert</button>`
125
+ : nothing;
126
+ return html `${this.#labelled.value
127
+ ? html `<div class="aster-field-head">
128
+ <label class="aster-field-label" id=${`${this.#uid}-label`} for=${this.#uid}>${label}</label>
129
+ ${meta && !state.required && !state.readOnly ? html `<span class="aster-optional">Optional</span>` : nothing}
130
+ ${revert}
131
+ </div>`
132
+ : nothing}
133
+ ${hidden ? this.#hiddenValue(c) : this.row(c)}
134
+ ${suggested ? html `<p class="aster-hint" id=${suggestedId}>Suggested value</p>` : nothing}
135
+ ${changed ? html `<span class="aster-sr-only" id=${pendingId}>${pendingText}</span>` : nothing}
136
+ ${reason && !sharedId ? html `<p class="aster-hint" id=${reasonId}>${reason}</p>` : nothing}
137
+ ${error ? html `<p class="aster-error" id=${errorId} role="alert">${error}</p>` : nothing}`;
138
+ }
139
+ }
@@ -0,0 +1,11 @@
1
+ import { type View } from "../base/element.ts";
2
+ import { BoundField, type ControlContext } from "./bound-field.ts";
3
+ /**
4
+ * A Boolean field as a checkbox. A null shows as mixed until set. Read-only
5
+ * keeps it focusable, so its reason can be reached: it's `aria-readonly`
6
+ * and a click doesn't change it.
7
+ */
8
+ export declare class Check extends BoundField {
9
+ focus(options?: FocusOptions): void;
10
+ protected control(c: ControlContext): View;
11
+ }
@@ -0,0 +1,26 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { define } from "../base/element.js";
3
+ import { BoundField } from "./bound-field.js";
4
+ /**
5
+ * A Boolean field as a checkbox. A null shows as mixed until set. Read-only
6
+ * keeps it focusable, so its reason can be reached: it's `aria-readonly`
7
+ * and a click doesn't change it.
8
+ */
9
+ export class Check extends BoundField {
10
+ focus(options) {
11
+ this.querySelector("input")?.focus(options);
12
+ }
13
+ control(c) {
14
+ return html `<label class="aster-check">
15
+ <input id=${c.id} type="checkbox" .checked=${c.value === true} .indeterminate=${c.value === null || c.value === undefined}
16
+ aria-label=${this.labelled ? nothing : c.label}
17
+ aria-readonly=${c.state.readOnly ? "true" : nothing}
18
+ aria-required=${c.state.required ? "true" : nothing}
19
+ @click=${(e) => c.state.readOnly && e.preventDefault()}
20
+ aria-invalid=${c.invalid ? "true" : nothing}
21
+ aria-describedby=${c.describedBy ?? nothing}
22
+ @change=${(e) => this.write(e.target.checked)}>
23
+ </label>`;
24
+ }
25
+ }
26
+ define("aster-check", Check);
@@ -0,0 +1,35 @@
1
+ import type { EditModel } from "@getaster/data";
2
+ import { AsterElement, type View } from "../base/element.ts";
3
+ export interface CompositeFieldContext<T = unknown> {
4
+ readonly value: T;
5
+ readonly disabled: boolean;
6
+ readonly readOnly: boolean;
7
+ readonly commit: (value: T) => void;
8
+ }
9
+ /** Related editors retain their own model fields, or edit an explicit application value. */
10
+ export declare class CompositeField<T = unknown> extends AsterElement {
11
+ #private;
12
+ get model(): EditModel | undefined;
13
+ set model(v: EditModel | undefined);
14
+ get fields(): readonly string[];
15
+ set fields(v: readonly string[]);
16
+ get label(): string;
17
+ set label(v: string);
18
+ get help(): string;
19
+ set help(v: string);
20
+ get error(): string;
21
+ set error(v: string);
22
+ get disabled(): boolean;
23
+ set disabled(v: boolean);
24
+ get readOnly(): boolean;
25
+ set readOnly(v: boolean);
26
+ get content(): View;
27
+ set content(v: View);
28
+ get value(): T | undefined;
29
+ set value(v: T | undefined);
30
+ get renderValue(): ((context: CompositeFieldContext<T | undefined>) => View) | undefined;
31
+ set renderValue(v: ((context: CompositeFieldContext<T | undefined>) => View) | undefined);
32
+ focus(options?: FocusOptions): void;
33
+ revert(): void;
34
+ protected render(): View;
35
+ }
@@ -0,0 +1,88 @@
1
+ import { batch } from "@preact/signals-core";
2
+ import { html, nothing } from "lit-html";
3
+ import { AsterElement, define, uid } from "../base/element.js";
4
+ /** Related editors retain their own model fields, or edit an explicit application value. */
5
+ export class CompositeField extends AsterElement {
6
+ #model = this.prop(undefined);
7
+ #fields = this.prop([]);
8
+ #label = this.prop("");
9
+ #help = this.prop("");
10
+ #error = this.prop("");
11
+ #disabled = this.prop(false);
12
+ #readOnly = this.prop(false);
13
+ #content = this.prop(nothing);
14
+ #value = this.prop(undefined);
15
+ #renderer = this.prop(undefined);
16
+ #id = uid("aster-composite-field");
17
+ get model() { return this.#model.value; }
18
+ set model(v) { this.#validate(this.#fields.peek(), v); this.#model.value = v; }
19
+ get fields() { return this.#fields.value; }
20
+ set fields(v) { this.#validate(v, this.#model.peek()); this.#fields.value = [...v]; }
21
+ get label() { return this.#label.value; }
22
+ set label(v) { this.#label.value = v; }
23
+ get help() { return this.#help.value; }
24
+ set help(v) { this.#help.value = v; }
25
+ get error() { return this.#error.value; }
26
+ set error(v) { this.#error.value = v; }
27
+ get disabled() { return this.#disabled.value; }
28
+ set disabled(v) { this.#disabled.value = v; }
29
+ get readOnly() { return this.#readOnly.value; }
30
+ set readOnly(v) { this.#readOnly.value = v; }
31
+ get content() { return this.#content.value; }
32
+ set content(v) { this.#content.value = v; }
33
+ get value() { return this.#value.value; }
34
+ set value(v) { this.#value.value = v; }
35
+ get renderValue() { return this.#renderer.value; }
36
+ set renderValue(v) { this.#renderer.value = v; }
37
+ focus(options) {
38
+ if (this.#disabled.peek())
39
+ return;
40
+ (this.querySelector("[aria-invalid=true]:not([disabled])") ?? this.querySelector("input:not([readonly]):not([disabled]),textarea:not([readonly]):not([disabled]),select:not([disabled]),button:not([disabled])"))?.focus(options);
41
+ }
42
+ revert() {
43
+ const model = this.#model.peek();
44
+ if (!model || this.#disabled.peek() || this.#readOnly.peek())
45
+ return;
46
+ batch(() => {
47
+ for (const name of this.#fields.peek()) {
48
+ const state = model.state(name).peek();
49
+ if (state.visible && !state.readOnly && !model.hidden.peek().has(name)) {
50
+ model.revert(name);
51
+ model.forcePending(name, false);
52
+ }
53
+ }
54
+ });
55
+ }
56
+ #validate(fields, model) {
57
+ if (new Set(fields).size !== fields.length || fields.some(f => !f || model && !model.type.field(f)))
58
+ throw new TypeError("CompositeField fields must be unique model field names");
59
+ }
60
+ render() {
61
+ const model = this.#model.value;
62
+ const fields = this.#fields.value;
63
+ const states = fields.map(f => model?.state(f).value);
64
+ this.hidden = !!model && fields.length > 0 && states.every(s => !s?.visible);
65
+ if (this.hidden)
66
+ return nothing;
67
+ const pending = fields.some(f => model?.pending(f).value);
68
+ this.toggleAttribute("data-group-pending", pending);
69
+ const disabled = this.#disabled.value;
70
+ const readOnly = this.#readOnly.value;
71
+ const error = this.#error.value;
72
+ const renderer = this.#renderer.value;
73
+ const descriptions = [this.#help.value ? `${this.#id}-help` : "", error ? `${this.#id}-error` : ""].filter(Boolean).join(" ");
74
+ return html `<fieldset class="aster-composite-field" ?disabled=${disabled || readOnly && !renderer} aria-describedby=${descriptions || nothing}>
75
+ <legend>${this.#label.value}</legend>
76
+ ${this.#help.value ? html `<p class="aster-hint" id=${`${this.#id}-help`}>${this.#help.value}</p>` : nothing}
77
+ <div class="aster-composite-content">${renderer ? renderer({ value: this.#value.value, disabled, readOnly, commit: value => {
78
+ if (this.#disabled.peek() || this.#readOnly.peek())
79
+ return;
80
+ this.#value.value = value;
81
+ this.emit("value-change", { value });
82
+ } }) : this.#content.value}</div>
83
+ ${pending && !disabled && !readOnly ? html `<button type="button" class="aster-revert" aria-label=${`Revert ${this.#label.value}`} @click=${() => this.revert()}>Revert group</button>` : nothing}
84
+ ${error ? html `<p class="aster-error" role="alert" id=${`${this.#id}-error`}>${error}</p>` : nothing}
85
+ </fieldset>`;
86
+ }
87
+ }
88
+ define("aster-composite-field", CompositeField);
@@ -0,0 +1,29 @@
1
+ import { type View } from "../base/element.ts";
2
+ import { BoundField, type ControlContext } from "./bound-field.ts";
3
+ /**
4
+ * A text field. Bound to an edit model's field, it shows that field's value
5
+ * and writes every keystroke back, parsed by the field's type (numbers,
6
+ * decimals, dates); text that doesn't parse stays as typed, and validation
7
+ * says what's wrong. Without a model it shows `value`, and `readOnly`
8
+ * selects everything on focus.
9
+ *
10
+ * The frame owns the focus ring. `trailing` renders beside the frame (an
11
+ * Invite button), so the error still spans under both.
12
+ */
13
+ export declare class Field extends BoundField {
14
+ #private;
15
+ get value(): string;
16
+ set value(v: string);
17
+ get placeholder(): string;
18
+ set placeholder(v: string);
19
+ get readOnly(): boolean;
20
+ set readOnly(v: boolean);
21
+ /** The `inputmode` to offer, when the field's type doesn't say (a number kept as text). */
22
+ get keyboard(): string;
23
+ set keyboard(v: string);
24
+ get trailing(): View;
25
+ set trailing(v: View);
26
+ focus(options?: FocusOptions): void;
27
+ protected row(c: ControlContext): View;
28
+ protected control(c: ControlContext): View;
29
+ }
@@ -0,0 +1,75 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { inputView } from "./input.js";
3
+ import { define } from "../base/element.js";
4
+ import { BoundField } from "./bound-field.js";
5
+ import { formatValue, inputKind, parseInput } from "./values.js";
6
+ /**
7
+ * A text field. Bound to an edit model's field, it shows that field's value
8
+ * and writes every keystroke back, parsed by the field's type (numbers,
9
+ * decimals, dates); text that doesn't parse stays as typed, and validation
10
+ * says what's wrong. Without a model it shows `value`, and `readOnly`
11
+ * selects everything on focus.
12
+ *
13
+ * The frame owns the focus ring. `trailing` renders beside the frame (an
14
+ * Invite button), so the error still spans under both.
15
+ */
16
+ export class Field extends BoundField {
17
+ #value = this.prop("");
18
+ #placeholder = this.prop("");
19
+ #readonly = this.prop(false);
20
+ #trailing = this.prop(nothing);
21
+ #keyboard = this.prop("");
22
+ get value() {
23
+ return this.#value.value;
24
+ }
25
+ set value(v) {
26
+ this.#value.value = v;
27
+ }
28
+ get placeholder() {
29
+ return this.#placeholder.value;
30
+ }
31
+ set placeholder(v) {
32
+ this.#placeholder.value = v;
33
+ }
34
+ get readOnly() {
35
+ return this.#readonly.value;
36
+ }
37
+ set readOnly(v) {
38
+ this.#readonly.value = v;
39
+ }
40
+ /** The `inputmode` to offer, when the field's type doesn't say (a number kept as text). */
41
+ get keyboard() {
42
+ return this.#keyboard.value;
43
+ }
44
+ set keyboard(v) {
45
+ this.#keyboard.value = v;
46
+ }
47
+ get trailing() {
48
+ return this.#trailing.value;
49
+ }
50
+ set trailing(v) {
51
+ this.#trailing.value = v;
52
+ }
53
+ focus(options) {
54
+ this.querySelector("input, textarea")?.focus(options);
55
+ }
56
+ row(c) {
57
+ return html `<div class="aster-field-row">${this.control(c)}${this.#trailing.value}</div>`;
58
+ }
59
+ control(c) {
60
+ const bound = this.model !== undefined;
61
+ // While typing, a Decimal shows exactly what was typed, not padded to its scale.
62
+ const text = bound ? (typeof c.value === "string" && c.meta?.type === "Edm.Decimal" ? c.value : formatValue(c.meta, c.value)) : this.#value.value;
63
+ const readOnly = this.#readonly.value || c.state.readOnly;
64
+ const kind = inputKind(c.meta);
65
+ return inputView({ id: c.id, value: text, type: kind.type, keyboard: this.#keyboard.value || kind.inputmode || "",
66
+ ...(this.labelled ? {} : { label: c.label }), placeholder: this.#placeholder.value, readOnly, required: c.state.required,
67
+ invalid: c.invalid, ...(c.describedBy ? { describedBy: c.describedBy } : {}), ...(isProse(c.meta) ? { rows: 2 } : {}),
68
+ input: (text) => this.write(parseInput(c.meta, text)) });
69
+ }
70
+ }
71
+ /** Long text (over 500 characters allowed) edits in a textarea that grows with it. */
72
+ function isProse(field) {
73
+ return field?.type === "Edm.String" && !field.allowedValues && (field.maxLength ?? 0) > 500;
74
+ }
75
+ define("aster-field", Field);
@@ -0,0 +1,15 @@
1
+ /** Scalar input chrome shared by record fields and standalone query drafts. */
2
+ export declare function inputView(p: {
3
+ readonly value: string;
4
+ readonly input: (text: string) => void;
5
+ readonly id?: string;
6
+ readonly label?: string;
7
+ readonly type?: string;
8
+ readonly keyboard?: string;
9
+ readonly placeholder?: string;
10
+ readonly readOnly?: boolean;
11
+ readonly required?: boolean;
12
+ readonly invalid?: boolean;
13
+ readonly describedBy?: string;
14
+ readonly rows?: number;
15
+ }): import("lit-html").TemplateResult<1>;
@@ -0,0 +1,18 @@
1
+ import { html, nothing } from "lit-html";
2
+ import { live } from "lit-html/directives/live.js";
3
+ /** Scalar input chrome shared by record fields and standalone query drafts. */
4
+ export function inputView(p) {
5
+ return html `<label class=${p.rows ? "aster-field aster-field-prose" : "aster-field"} data-ring-owner>
6
+ ${p.rows ? html `<textarea id=${p.id ?? nothing} rows=${p.rows} .value=${live(p.value)}
7
+ aria-label=${p.label ?? nothing} placeholder=${p.placeholder || nothing} ?readonly=${p.readOnly}
8
+ aria-required=${p.required ? "true" : nothing} aria-invalid=${p.invalid ? "true" : nothing}
9
+ aria-describedby=${p.describedBy ?? nothing}
10
+ @input=${(e) => p.input(e.target.value)}></textarea>`
11
+ : html `<input id=${p.id ?? nothing} type=${p.type ?? "text"} inputmode=${p.keyboard || nothing} .value=${live(p.value)}
12
+ aria-label=${p.label ?? nothing} placeholder=${p.placeholder || nothing} ?readonly=${p.readOnly}
13
+ aria-required=${p.required ? "true" : nothing} aria-invalid=${p.invalid ? "true" : nothing}
14
+ aria-describedby=${p.describedBy ?? nothing} autocomplete="off" spellcheck="false"
15
+ @focus=${(e) => p.readOnly && e.target.select()}
16
+ @input=${(e) => p.input(e.target.value)}>`}
17
+ </label>`;
18
+ }
@@ -0,0 +1,33 @@
1
+ import type { Field as FieldMeta, Value } from "@getaster/data";
2
+ import { type View } from "../base/element.ts";
3
+ import { BoundField, type ControlContext } from "./bound-field.ts";
4
+ export type NumericValue = string | number | null;
5
+ export type NumericType = "decimal" | "integer" | "float";
6
+ export declare class NumberField extends BoundField {
7
+ #private;
8
+ get value(): NumericValue;
9
+ set value(v: NumericValue);
10
+ get numericType(): NumericType;
11
+ set numericType(v: NumericType);
12
+ get min(): string | number | undefined;
13
+ set min(v: string | number | undefined);
14
+ get max(): string | number | undefined;
15
+ set max(v: string | number | undefined);
16
+ get step(): string | number | undefined;
17
+ set step(v: string | number | undefined);
18
+ get stepButtons(): boolean;
19
+ set stepButtons(v: boolean);
20
+ get prefixText(): string;
21
+ set prefixText(v: string);
22
+ get suffixText(): string;
23
+ set suffixText(v: string);
24
+ get placeholder(): string;
25
+ set placeholder(v: string);
26
+ get readOnly(): boolean;
27
+ set readOnly(v: boolean);
28
+ get format(): ((value: NumericValue) => string) | undefined;
29
+ set format(v: ((value: NumericValue) => string) | undefined);
30
+ focus(options?: FocusOptions): void;
31
+ protected validation(meta: FieldMeta | undefined, boundValue: Value | undefined): string | undefined;
32
+ protected control(c: ControlContext): View;
33
+ }