@getaster/elements 0.3.5 → 0.4.5

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 (93) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +13 -4
  3. package/bin/aster-theme-check.js +35 -4
  4. package/dist/base/element.d.ts +8 -1
  5. package/dist/base/element.js +7 -2
  6. package/dist/button/button.d.ts +17 -2
  7. package/dist/button/button.js +42 -8
  8. package/dist/composition/builtins.d.ts +623 -1
  9. package/dist/composition/builtins.js +232 -4
  10. package/dist/date/calendar.d.ts +3 -0
  11. package/dist/date/calendar.js +8 -2
  12. package/dist/date/date-field.d.ts +23 -3
  13. package/dist/date/date-field.js +77 -9
  14. package/dist/date/dates.d.ts +10 -2
  15. package/dist/date/dates.js +23 -3
  16. package/dist/dialog/config.d.ts +4 -0
  17. package/dist/dialog/config.js +8 -0
  18. package/dist/dialog/dialog.d.ts +34 -6
  19. package/dist/dialog/dialog.js +151 -14
  20. package/dist/field/bound-field.d.ts +17 -1
  21. package/dist/field/bound-field.js +68 -7
  22. package/dist/field/check.js +1 -0
  23. package/dist/field/field.js +1 -1
  24. package/dist/field/input.d.ts +1 -0
  25. package/dist/field/input.js +2 -2
  26. package/dist/field/number-field.js +2 -2
  27. package/dist/filter/filter-builder.d.ts +7 -1
  28. package/dist/filter/filter-builder.js +8 -1
  29. package/dist/form/controls.d.ts +40 -8
  30. package/dist/form/controls.js +58 -15
  31. package/dist/form/custom-field.d.ts +1 -0
  32. package/dist/form/custom-field.js +23 -0
  33. package/dist/form/features.d.ts +27 -0
  34. package/dist/form/features.js +52 -0
  35. package/dist/form/form.d.ts +67 -7
  36. package/dist/form/form.js +267 -24
  37. package/dist/form/policy.d.ts +29 -0
  38. package/dist/form/policy.js +16 -0
  39. package/dist/grid/appearance.d.ts +365 -0
  40. package/dist/grid/appearance.js +814 -0
  41. package/dist/grid/features.d.ts +38 -0
  42. package/dist/grid/features.js +78 -0
  43. package/dist/grid/grid.d.ts +216 -4
  44. package/dist/grid/grid.js +1810 -308
  45. package/dist/grid/policy.d.ts +58 -0
  46. package/dist/grid/policy.js +17 -0
  47. package/dist/grid/view.d.ts +32 -2
  48. package/dist/grid/view.js +89 -4
  49. package/dist/index.d.ts +14 -11
  50. package/dist/index.js +2 -1
  51. package/dist/menu/config.d.ts +7 -0
  52. package/dist/menu/config.js +44 -0
  53. package/dist/menu/menu.d.ts +23 -0
  54. package/dist/menu/menu.js +69 -17
  55. package/dist/picker/picker.js +8 -3
  56. package/dist/section/accordion.d.ts +26 -0
  57. package/dist/section/accordion.js +86 -8
  58. package/dist/section/policy.d.ts +8 -0
  59. package/dist/section/policy.js +17 -0
  60. package/dist/section/section.d.ts +30 -0
  61. package/dist/section/section.js +106 -3
  62. package/dist/select/choice-element.d.ts +12 -0
  63. package/dist/select/choice-element.js +36 -0
  64. package/dist/select/choice.d.ts +12 -2
  65. package/dist/select/choice.js +93 -28
  66. package/dist/select/select.d.ts +12 -0
  67. package/dist/select/select.js +37 -1
  68. package/dist/styles/contract.d.ts +14 -0
  69. package/dist/styles/contract.js +15 -0
  70. package/dist/switch/switch.d.ts +16 -0
  71. package/dist/switch/switch.js +56 -6
  72. package/dist/tree/navigator.d.ts +3 -0
  73. package/dist/tree/navigator.js +26 -2
  74. package/dist/window/chrome.d.ts +5 -1
  75. package/dist/window/chrome.js +4 -3
  76. package/dist/window/config.d.ts +71 -0
  77. package/dist/window/config.js +40 -0
  78. package/dist/window/window.d.ts +51 -13
  79. package/dist/window/window.js +233 -38
  80. package/dist/yaml.d.ts +19 -0
  81. package/dist/yaml.js +57 -0
  82. package/package.json +10 -4
  83. package/styles/aster-ui.css +181 -10
  84. package/styles/base.css +14 -0
  85. package/styles/contract.ts +16 -0
  86. package/styles/elements.css +167 -10
  87. package/styles/fonts/OFL-bricolage-grotesque.txt +93 -0
  88. package/styles/fonts/OFL-fraunces.txt +93 -0
  89. package/styles/fonts/bricolage-grotesque-latin-opsz-normal.woff2 +0 -0
  90. package/styles/fonts/fraunces-latin-full-normal.woff2 +0 -0
  91. package/styles/fonts.css +17 -0
  92. package/styles/theme-harbour.css +5 -0
  93. package/styles/theme-lamplight.css +5 -0
@@ -22,9 +22,12 @@ import { DateField } from "../date/date-field.js";
22
22
  import { Choice } from "../select/choice-element.js";
23
23
  import { Switch } from "../switch/switch.js";
24
24
  import { Grid } from "../grid/grid.js";
25
+ import { validateFeatures } from "../grid/features.js";
26
+ import { validateGridLabels, validateGridPresentation } from "../grid/appearance.js";
25
27
  import { TileGrid } from "../tile-grid/tile-grid.js";
26
28
  import { Listbox } from "../listbox/listbox.js";
27
29
  import { Form } from "../form/form.js";
30
+ import { validateFormFeatures } from "../form/features.js";
28
31
  import { ListEditor } from "../list-edit/list-edit.js";
29
32
  import { RuleEditor } from "../rule/rule-editor.js";
30
33
  import { FilterBuilder } from "../filter/filter-builder.js";
@@ -37,6 +40,7 @@ import { Slicer } from "../slicer/slicer.js";
37
40
  import { ComponentHost } from "../component/component.js";
38
41
  import { Window } from "../window/window.js";
39
42
  import { Dialog } from "../dialog/dialog.js";
43
+ import { Menu } from "../menu/menu.js";
40
44
  import { Dashboard } from "../dashboard/dashboard.js";
41
45
  import { CommandBar } from "../composition/command-bar.js";
42
46
  const object = (value) => !!value && typeof value === "object" && [Object.prototype, null].includes(Object.getPrototypeOf(value));
@@ -74,20 +78,136 @@ const numeric = (v) => {
74
78
  }
75
79
  };
76
80
  const sourceOptions = (v) => v === undefined || v instanceof Collection || v instanceof PickerOptions;
77
- const formItem = checked("field name, configured field or derived value", v => typeof v === "string" || object(v) && optionalNumber(v.span) && (typeof v.field === "string" && sourceOptions(v.options)
78
- || typeof v.title === "string" && t.signal(t.string).validate(v.value)));
81
+ const formControlName = checked("form control name", (v) => v === "text" || v === "number" || v === "date" || v === "check" || v === "select" || v === "picker");
82
+ const formItem = checked("field name, configured field or derived value", v => typeof v === "string" || (object(v) && optionalNumber(v.span) && ((typeof v.field === "string" && sourceOptions(v.options)
83
+ && (v.title === undefined || typeof v.title === "string")
84
+ && (v.hint === undefined || typeof v.hint === "string")
85
+ && (v.placeholder === undefined || typeof v.placeholder === "string")
86
+ && optionalBoolean(v.readOnly) && optionalBoolean(v.disabled) && optionalBoolean(v.required)
87
+ && (v.control === undefined || formControlName.validate(v.control)))
88
+ || (typeof v.title === "string" && t.signal(t.string).validate(v.value)))));
79
89
  const sections = t.array(t.object({ title: t.string, columns: t.optional(t.positiveInteger), open: t.optional(t.boolean), items: t.array(formItem) }));
90
+ const formPolicy = t.object({ readOnly: t.optional(t.boolean), disabled: t.optional(t.boolean) });
91
+ const formChrome = t.object({
92
+ columns: t.optional(t.positiveInteger),
93
+ titleOrientation: t.optional(literals("top", "start")),
94
+ showInlineErrors: t.optional(t.boolean),
95
+ errorSummary: t.optional(t.boolean),
96
+ autoFocus: t.optional(t.boolean),
97
+ });
98
+ const formFeatures = t.custom("form feature list", (value) => {
99
+ try {
100
+ validateFormFeatures(value);
101
+ return true;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }, { type: "array", items: { type: "object" } });
107
+ const isoDate = checked("date (YYYY-MM-DD)", (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v));
80
108
  const gridView = checked("Grid view", v => object(v)
81
109
  && Array.isArray(v.columns) && v.columns.every(c => object(c) && typeof c.field === "string" && optionalNumber(c.width) && optionalBoolean(c.hidden) && (c.sortField === undefined || typeof c.sortField === "string" && c.sortField.trim().length > 0))
82
110
  && Array.isArray(v.sort) && v.sort.every(k => object(k) && typeof k.field === "string" && optionalBoolean(k.descending))
83
111
  && object(v.filters) && Object.values(v.filters).every(c => object(c) && typeof c.op === "string")
84
112
  && typeof v.frozen === "number" && Number.isSafeInteger(v.frozen) && v.frozen >= 0
85
113
  && (v.groupBy === undefined || typeof v.groupBy === "string" || Array.isArray(v.groupBy) && v.groupBy.every(k => typeof k === "string")));
114
+ const gridChrome = t.object({
115
+ toolbar: t.optional(t.boolean),
116
+ header: t.optional(t.boolean),
117
+ filterRow: t.optional(checked('true, false or "auto"', v => v === "auto" || typeof v === "boolean")),
118
+ headerMenu: t.optional(t.boolean),
119
+ pagingBar: t.optional(t.boolean),
120
+ });
121
+ const gridToolbar = t.object({
122
+ count: t.optional(t.boolean),
123
+ sort: t.optional(t.boolean),
124
+ filters: t.optional(t.boolean),
125
+ advanced: t.optional(t.boolean),
126
+ columns: t.optional(t.boolean),
127
+ views: t.optional(t.boolean),
128
+ expandAll: t.optional(t.boolean),
129
+ extra: t.optional(checked("Grid.toolbar.extra", v => v === undefined || view(v))),
130
+ });
131
+ // Capability switches in a composition catalog are plain data: a boolean or a
132
+ // field-name allowlist. Predicates remain a JavaScript-only setter value.
133
+ const gridCapability = t.custom("boolean or field allowlist", (value) => typeof value === "boolean" || Array.isArray(value) && value.every((field) => typeof field === "string"), { anyOf: [{ type: "boolean" }, { type: "array", items: { type: "string" } }] });
134
+ const fieldPolicy = t.object({
135
+ sortable: t.optional(gridCapability), filterable: t.optional(gridCapability), groupable: t.optional(gridCapability),
136
+ resizable: t.optional(gridCapability), movable: t.optional(gridCapability), hideable: t.optional(gridCapability),
137
+ freezable: t.optional(gridCapability), editable: t.optional(gridCapability),
138
+ });
139
+ const fieldPolicies = t.custom("field policy record", (value) => object(value) && Object.values(value).every((entry) => fieldPolicy.validate(entry)), { type: "object", additionalProperties: fieldPolicy.schema ?? {} });
140
+ const gridPolicy = t.object({
141
+ sortable: t.optional(gridCapability),
142
+ filterable: t.optional(gridCapability),
143
+ groupable: t.optional(gridCapability),
144
+ resizable: t.optional(gridCapability),
145
+ movable: t.optional(gridCapability),
146
+ hideable: t.optional(gridCapability),
147
+ freezable: t.optional(gridCapability),
148
+ editable: t.optional(gridCapability),
149
+ selectable: t.optional(t.boolean),
150
+ fields: t.optional(fieldPolicies),
151
+ });
152
+ const gridFeatures = t.custom("grid feature list", (value) => {
153
+ try {
154
+ validateFeatures(value);
155
+ return true;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }, { type: "array", items: { type: "object" } });
161
+ // Presentation is plain data in a catalog; JS-only class predicates and hilite
162
+ // tests fail validation because `validateGridPresentation` rejects functions.
163
+ const gridPresentation = t.custom("grid presentation", (value) => {
164
+ try {
165
+ validateGridPresentation(value);
166
+ return true;
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }, { type: "object" });
172
+ const gridLabels = t.custom("grid label bag", (value) => {
173
+ try {
174
+ validateGridLabels(value);
175
+ return true;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }, { type: "object", additionalProperties: { type: "string" } });
86
181
  const milliseconds = t.custom("nonnegative milliseconds", (value) => Number.isSafeInteger(value) && typeof value === "number" && value >= 0 && value <= 2_147_483_647, { type: "integer", minimum: 0, maximum: 2_147_483_647 });
182
+ const sectionChrome = t.object({
183
+ titleBar: t.optional(t.boolean),
184
+ fold: t.optional(t.boolean),
185
+ close: t.optional(t.boolean),
186
+ footer: t.optional(t.boolean),
187
+ status: t.optional(literals("collapsed", "always")),
188
+ });
189
+ const windowChrome = t.object({
190
+ header: t.optional(t.boolean),
191
+ title: t.optional(t.boolean),
192
+ close: t.optional(t.boolean),
193
+ minimize: t.optional(t.boolean),
194
+ maximize: t.optional(t.boolean),
195
+ resizer: t.optional(t.boolean),
196
+ footer: t.optional(t.boolean),
197
+ statusBar: t.optional(t.boolean),
198
+ });
199
+ const positiveNumber = t.custom("positive finite number", (v) => typeof v === "number" && Number.isFinite(v) && v > 0, { type: "number", exclusiveMinimum: 0 });
200
+ const windowPlacement = t.object({
201
+ state: literals("normal", "maximized", "minimized"),
202
+ bounds: t.optional(t.object({ x: t.number, y: t.number, width: positiveNumber, height: positiveNumber })),
203
+ restoreMaximized: t.optional(t.boolean),
204
+ });
205
+ const initialFocus = checked('"first", "dialog", "none" or a focus function', v => v === undefined || v === "first" || v === "dialog" || v === "none" || fn(v));
87
206
  const progressTiming = t.object({ delayMs: t.optional(milliseconds), minVisibleMs: t.optional(milliseconds) });
88
207
  const bound = {
89
208
  model: t.optional(t.instance(EditModel)), field: t.string, label: t.string,
90
209
  labelled: t.boolean, sharedReasons: t.instance((Map)),
210
+ hint: t.string, showErrors: t.boolean, validateOn: literals("submit", "change", "exit"),
91
211
  };
92
212
  /** Reusable public control contracts. Select entries explicitly for an application's allowlist. */
93
213
  export const asterComponents = Object.freeze({
@@ -97,6 +217,9 @@ export const asterComponents = Object.freeze({
97
217
  confirmLabel: t.optional(t.string),
98
218
  busy: bindable(t.boolean),
99
219
  variant: literals("secondary", "primary"),
220
+ tone: literals("secondary", "primary", "danger"),
221
+ size: literals("sm", "md", "lg"),
222
+ disabled: t.boolean,
100
223
  type: literals("button", "submit"),
101
224
  icon: checked("Button.icon", view),
102
225
  reason: t.optional(t.string),
@@ -131,6 +254,8 @@ export const asterComponents = Object.freeze({
131
254
  Accordion: element()("aster-accordion", {
132
255
  items: checked("accordion items", v => Array.isArray(v) && new Set(v.map(i => i?.id)).size === v.length && v.every(i => object(i) && typeof i.id === "string" && i.id.length > 0 && typeof i.heading === "string" && !(i.body !== undefined && i.create !== undefined) && optionalBoolean(i.disabled) && (i.body === undefined || view(i.body)) && (i.summary === undefined || view(i.summary)) && (i.actions === undefined || view(i.actions)) && (i.create === undefined || fn(i.create)))),
133
256
  expanded: t.array(t.string), multiple: t.boolean, headingLevel: literals(2, 3, 4),
257
+ visibilityMode: literals("multiple", "mutex"),
258
+ canClose: t.boolean, canReorder: t.boolean, canCollapseAll: t.boolean,
134
259
  }),
135
260
  Check: element()("aster-check", {
136
261
  ...bound,
@@ -138,6 +263,10 @@ export const asterComponents = Object.freeze({
138
263
  Select: element()("aster-select", {
139
264
  ...bound,
140
265
  choices: t.optional(options),
266
+ placeholder: t.string,
267
+ filter: t.boolean,
268
+ clearable: t.boolean,
269
+ noResults: t.string,
141
270
  }),
142
271
  Picker: element()("aster-picker", {
143
272
  ...bound,
@@ -155,16 +284,29 @@ export const asterComponents = Object.freeze({
155
284
  }),
156
285
  DateField: element()("aster-date", {
157
286
  ...bound,
287
+ min: t.optional(isoDate),
288
+ max: t.optional(isoDate),
289
+ format: literals("medium", "short", "iso"),
290
+ placeholder: t.string,
291
+ dismissOnSelect: t.boolean,
158
292
  }),
159
293
  Choice: element()("aster-choice", {
160
294
  options: options,
161
295
  value: checked("Choice.value", v => v === undefined || value(v)),
162
296
  label: t.string,
163
297
  placeholder: t.string,
298
+ filter: t.boolean,
299
+ clearable: t.boolean,
300
+ noResults: t.string,
301
+ disabled: t.boolean,
164
302
  }),
165
303
  Switch: element()("aster-switch", {
166
304
  binding: checked("Switch.binding", v => v === undefined || object(v) && v.ctx instanceof DataContext && typeof v.set === "string" && typeof v.key === "string" && typeof v.field === "string"),
167
305
  label: t.string,
306
+ disabled: t.boolean,
307
+ readOnly: t.boolean,
308
+ onLabel: t.optional(t.string),
309
+ offLabel: t.optional(t.string),
168
310
  }),
169
311
  Grid: element()("aster-grid", {
170
312
  progressPolicy: progressTiming,
@@ -177,18 +319,55 @@ export const asterComponents = Object.freeze({
177
319
  persist: t.string,
178
320
  renderers: checked("Grid.renderers", v => object(v) && Object.values(v).every(fn)),
179
321
  formatters: checked("Grid.formatters", v => object(v) && Object.values(v).every(fn)),
322
+ presentation: gridPresentation,
323
+ labels: gridLabels,
180
324
  summaries: checked("Grid.summaries", v => Array.isArray(v) && v.every(s => object(s) && (s.label === undefined || typeof s.label === "string") && (s.method === "count" || ["sum", "min", "max", "average", "countdistinct"].includes(String(s.method)) && typeof s.field === "string"))),
181
325
  lookups: checked("Grid.lookups", v => object(v) && Object.values(v).every(l => object(l) && typeof l.set === "string" && typeof l.display === "string")),
182
326
  rowMenu: checked("Grid.rowMenu", fn),
183
- tree: checked("Grid.tree", v => v === undefined || object(v) && typeof v.parentField === "string" && optionalBoolean(v.draggable) && (v.filterMode === undefined || ["keepAncestors", "strict"].includes(String(v.filterMode)))),
327
+ columnMenu: checked("Grid.columnMenu", fn),
328
+ filterOperators: checked("Grid.filterOperators", v => object(v) && Object.values(v).every(list => Array.isArray(list) && list.every(op => typeof op === "string" && op.length > 0))),
329
+ tree: checked("Grid.tree", v => v === undefined || object(v) && typeof v.parentField === "string"
330
+ && optionalBoolean(v.draggable) && optionalBoolean(v.reparent) && optionalBoolean(v.dropOnLeaves) && optionalBoolean(v.cascadeSelection)
331
+ && optionalBoolean(v.showOpener) && (v.autoOpen === undefined || Number.isSafeInteger(v.autoOpen) && typeof v.autoOpen === "number" && v.autoOpen >= 0)
332
+ && (v.pageSize === undefined || t.positiveInteger.validate(v.pageSize)) && (v.matchLimit === undefined || t.positiveInteger.validate(v.matchLimit))
333
+ && (v.openLimit === undefined || t.positiveInteger.validate(v.openLimit))
334
+ && (v.folderIcon === undefined || view(v.folderIcon)) && (v.nodeIcon === undefined || view(v.nodeIcon))
335
+ && (v.filterMode === undefined || ["keepAncestors", "strict"].includes(String(v.filterMode)))),
184
336
  rowControls: checked("Grid.rowControls", v => v === undefined || fn(v)),
185
337
  expansion: checked("Grid.expansion", v => v === undefined || object(v) && fn(v.content) && optionalBoolean(v.single) && optionalNumber(v.limit) && (v.canExpand === undefined || fn(v.canExpand))),
186
- editable: t.boolean,
338
+ policy: gridPolicy,
339
+ features: gridFeatures,
340
+ sortable: gridCapability,
341
+ filterable: gridCapability,
342
+ groupable: gridCapability,
343
+ resizable: gridCapability,
344
+ movable: gridCapability,
345
+ hideable: gridCapability,
346
+ freezable: gridCapability,
347
+ editable: gridCapability,
348
+ multiSort: t.boolean,
349
+ editMode: literals("row", "cell"),
350
+ selectionAppearance: literals("row", "cell"),
351
+ selectionColumn: t.boolean,
187
352
  rules: checked("Grid.rules", object),
188
353
  empty: checked("Grid.empty", view),
354
+ loading: checked("Grid.loading", view),
355
+ loadingMessage: t.string,
356
+ error: checked("Grid.error", v => v === undefined || fn(v)),
357
+ autoFetch: t.boolean,
358
+ pageSize: t.positiveInteger,
359
+ keepPages: t.optional(t.positiveInteger),
360
+ exactCount: t.boolean,
189
361
  collection: t.optional(t.instance(Collection)),
190
362
  selection: t.optional(t.instance(Selection)),
191
363
  selectable: t.boolean,
364
+ chrome: gridChrome,
365
+ toolbar: gridToolbar,
366
+ showHeader: t.boolean,
367
+ showToolbar: t.boolean,
368
+ showFilterRow: t.boolean,
369
+ showHeaderMenu: t.boolean,
370
+ showPagingBar: t.boolean,
192
371
  }),
193
372
  TileGrid: element()("aster-tile-grid", {
194
373
  collection: t.optional(t.instance(Collection)),
@@ -215,6 +394,18 @@ export const asterComponents = Object.freeze({
215
394
  model: t.optional(t.instance(EditModel)),
216
395
  sections,
217
396
  headingLevel: literals(2, 3, 4),
397
+ policy: formPolicy,
398
+ readOnly: t.boolean,
399
+ disabled: t.boolean,
400
+ chrome: formChrome,
401
+ columns: t.positiveInteger,
402
+ titleOrientation: literals("top", "start"),
403
+ showInlineErrors: t.boolean,
404
+ errorSummary: t.boolean,
405
+ autoFocus: t.boolean,
406
+ features: formFeatures,
407
+ validateOn: literals("submit", "change", "exit"),
408
+ saveOnEnter: t.boolean,
218
409
  }),
219
410
  ListEditor: element()("aster-list-editor", {
220
411
  list: t.optional(t.instance(ListEdit)),
@@ -238,6 +429,7 @@ export const asterComponents = Object.freeze({
238
429
  model: t.optional(t.instance(FilterDraft)),
239
430
  label: t.string,
240
431
  readOnlyReason: t.optional(t.string),
432
+ operators: checked("FilterBuilder.operators", v => v === undefined || fn(v)),
241
433
  }),
242
434
  TreeNavigator: element()("aster-tree-navigator", {
243
435
  tree: t.optional(t.instance(Tree)),
@@ -246,6 +438,7 @@ export const asterComponents = Object.freeze({
246
438
  choosable: checked("TreeNavigator.choosable", v => v === undefined || fn(v)),
247
439
  headingLevel: literals(1, 2, 3, 4, 5, 6),
248
440
  chooseLabel: t.string,
441
+ search: t.boolean,
249
442
  }),
250
443
  Tabs: element()("aster-tabs", {
251
444
  renderPanel: checked("Tabs.renderPanel", v => v === undefined || fn(v)),
@@ -257,12 +450,16 @@ export const asterComponents = Object.freeze({
257
450
  }),
258
451
  Section: element()("aster-section", {
259
452
  actions: checked("Section.actions", view),
453
+ chrome: sectionChrome,
454
+ collapsible: t.boolean,
260
455
  disabled: t.boolean,
456
+ footer: checked("Section.footer", view),
261
457
  heading: t.string,
262
458
  open: t.boolean,
263
459
  body: checked("Section.body", view),
264
460
  level: literals(2, 3, 4),
265
461
  summary: checked("Section.summary", view),
462
+ canClose: t.boolean,
266
463
  }),
267
464
  Split: element()("aster-split", {
268
465
  orientation: literals("horizontal", "vertical"), size: t.number, collapsible: t.boolean, collapsed: t.boolean, responsive: t.boolean,
@@ -296,10 +493,41 @@ export const asterComponents = Object.freeze({
296
493
  managed: t.boolean,
297
494
  preserveContent: t.boolean,
298
495
  beforeClose: checked("Window.beforeClose", v => v === undefined || fn(v)),
496
+ chrome: windowChrome,
497
+ footer: checked("Window.footer", view),
498
+ status: checked("Window.status", view),
499
+ initialPlacement: windowPlacement,
500
+ modal: t.boolean,
501
+ dismissOnEscape: t.boolean,
502
+ keepInParentRect: t.boolean,
503
+ autoCenter: t.boolean,
504
+ showHeader: t.boolean,
505
+ showTitle: t.boolean,
506
+ showCloseButton: t.boolean,
507
+ showMinimizeButton: t.boolean,
508
+ showMaximizeButton: t.boolean,
509
+ showResizer: t.boolean,
510
+ showFooter: t.boolean,
511
+ showStatusBar: t.boolean,
299
512
  }),
300
513
  Dialog: element()("aster-dialog", {
301
514
  content: checked("Dialog.content", view),
302
515
  labelledBy: t.optional(t.string),
516
+ title: t.string,
517
+ header: t.boolean,
518
+ footer: checked("Dialog.footer", view),
519
+ showCloseButton: t.boolean,
520
+ modal: t.boolean,
521
+ dismissOnEscape: t.boolean,
522
+ dismissOnBackdrop: t.boolean,
523
+ initialFocus,
524
+ }),
525
+ Menu: element()("aster-menu", {
526
+ showIcons: t.boolean,
527
+ showShortcuts: t.boolean,
528
+ submenuDirection: literals("auto", "start", "end"),
529
+ autoDismiss: t.boolean,
530
+ emptyMessage: t.string,
303
531
  }),
304
532
  Dashboard: element()("aster-dashboard", {
305
533
  label: t.string,
@@ -16,6 +16,9 @@ export interface CalendarProps {
16
16
  readonly value: string | undefined;
17
17
  readonly today: string;
18
18
  readonly label: string;
19
+ /** Inclusive bounds, `YYYY-MM-DD`; days outside are shown but can't be chosen. */
20
+ readonly min?: string | undefined;
21
+ readonly max?: string | undefined;
19
22
  readonly choose: (date: string) => void;
20
23
  /** Closed without a choice: Escape, or a press outside. */
21
24
  readonly dismiss: (refocus: boolean) => void;
@@ -59,6 +59,11 @@ export function calendarView(state, p) {
59
59
  queueMicrotask(() => document.getElementById(`${p.id}-${to}`)?.focus());
60
60
  };
61
61
  const page = (by) => (state.focused.value = shift(focused, by));
62
+ const allowed = (day) => (p.min === undefined || day >= p.min) && (p.max === undefined || day <= p.max);
63
+ const choose = (day) => {
64
+ if (allowed(day))
65
+ p.choose(day);
66
+ };
62
67
  const grid = (e) => {
63
68
  const to = gridKey(e, focused);
64
69
  if (to) {
@@ -67,7 +72,7 @@ export function calendarView(state, p) {
67
72
  }
68
73
  else if (e.key === "Enter" || e.key === " ") {
69
74
  e.preventDefault();
70
- p.choose(focused);
75
+ choose(focused);
71
76
  }
72
77
  };
73
78
  const dialog = (e) => {
@@ -106,8 +111,9 @@ export function calendarView(state, p) {
106
111
  ? html `<td></td>`
107
112
  : html `<td role="gridcell" id=${`${p.id}-${day}`} tabindex=${day === focused ? "0" : "-1"}
108
113
  aria-selected=${day === p.value ? "true" : nothing} aria-current=${day === p.today ? "date" : nothing}
114
+ aria-disabled=${allowed(day) ? nothing : "true"}
109
115
  aria-label=${DAY.format(Date.parse(`${day}T12:00:00Z`))}
110
- @click=${() => p.choose(day)}>${Number(day.slice(8))}</td>`)}</tr>`)}
116
+ @click=${() => choose(day)}>${Number(day.slice(8))}</td>`)}</tr>`)}
111
117
  </tbody>
112
118
  </table>
113
119
  </div>`;
@@ -1,5 +1,7 @@
1
+ import type { Field as FieldMeta, Value } from "@getaster/data";
1
2
  import { type View } from "../base/element.ts";
2
3
  import { BoundField, type ControlContext } from "../field/bound-field.ts";
4
+ import { type DateFormat } from "./dates.ts";
3
5
  /**
4
6
  * A date field: typed dates in the forms people write them ("7 Oct",
5
7
  * "7/10/2026", "2026-10-07"), shown as "7 Oct 2026", with a calendar
@@ -7,12 +9,30 @@ import { BoundField, type ControlContext } from "../field/bound-field.ts";
7
9
  * typed while the input has focus; text that isn't a date stays text, and
8
10
  * validation says so.
9
11
  *
10
- * The calendar button takes focus on press in every engine (macOS engines
11
- * don't focus buttons on click), so focus never falls to the body between
12
- * the input and the calendar: a grid row being edited would end its edit.
12
+ * `min` and `max` bound the calendar and the value; `format` chooses how a
13
+ * date is written. The calendar button takes focus on press in every engine
14
+ * (macOS engines don't focus buttons on click), so focus never falls to the
15
+ * body between the input and the calendar: a grid row being edited would end
16
+ * its edit.
13
17
  */
14
18
  export declare class DateField extends BoundField {
15
19
  #private;
20
+ /** The earliest date that can be chosen, `YYYY-MM-DD`, inclusive. */
21
+ get min(): string | undefined;
22
+ set min(v: string | undefined);
23
+ /** The latest date that can be chosen, `YYYY-MM-DD`, inclusive. */
24
+ get max(): string | undefined;
25
+ set max(v: string | undefined);
26
+ /** How a date is written in the input: `"medium"` (default), `"short"` or `"iso"`. */
27
+ get format(): DateFormat;
28
+ set format(v: DateFormat);
29
+ /** Placeholder text. Default: "None" for a nullable field, otherwise a date hint. */
30
+ get placeholder(): string;
31
+ set placeholder(v: string);
32
+ /** Whether choosing a day closes the calendar. Default `true`. */
33
+ get dismissOnSelect(): boolean;
34
+ set dismissOnSelect(v: boolean);
16
35
  focus(options?: FocusOptions): void;
36
+ protected validation(_meta: FieldMeta | undefined, value: Value | undefined): string | undefined;
17
37
  protected control(c: ControlContext): View;
18
38
  }
@@ -4,7 +4,7 @@ import { define } from "../base/element.js";
4
4
  import { BoundField } from "../field/bound-field.js";
5
5
  import { anchorStyle } from "../overlay/anchor.js";
6
6
  import { CalendarState, calendarView, closeOutside } from "./calendar.js";
7
- import { formatDate, parseDate, today } from "./dates.js";
7
+ import { formatDate, isIsoDate, parseDate, today } from "./dates.js";
8
8
  /**
9
9
  * A date field: typed dates in the forms people write them ("7 Oct",
10
10
  * "7/10/2026", "2026-10-07"), shown as "7 Oct 2026", with a calendar
@@ -12,22 +12,85 @@ import { formatDate, parseDate, today } from "./dates.js";
12
12
  * typed while the input has focus; text that isn't a date stays text, and
13
13
  * validation says so.
14
14
  *
15
- * The calendar button takes focus on press in every engine (macOS engines
16
- * don't focus buttons on click), so focus never falls to the body between
17
- * the input and the calendar: a grid row being edited would end its edit.
15
+ * `min` and `max` bound the calendar and the value; `format` chooses how a
16
+ * date is written. The calendar button takes focus on press in every engine
17
+ * (macOS engines don't focus buttons on click), so focus never falls to the
18
+ * body between the input and the calendar: a grid row being edited would end
19
+ * its edit.
18
20
  */
19
21
  export class DateField extends BoundField {
20
22
  #calendar = new CalendarState();
21
23
  /** What's being typed, while the input has focus. */
22
24
  #draft = this.prop(undefined);
25
+ #min = this.prop(undefined);
26
+ #max = this.prop(undefined);
27
+ #format = this.prop("medium");
28
+ #placeholder = this.prop("");
29
+ #dismissOnSelect = this.prop(true);
30
+ /** The earliest date that can be chosen, `YYYY-MM-DD`, inclusive. */
31
+ get min() {
32
+ return this.#min.value;
33
+ }
34
+ set min(v) {
35
+ if (v !== undefined && !isIsoDate(v))
36
+ throw new TypeError("min must be a date, like 2026-10-07");
37
+ this.#min.value = v;
38
+ }
39
+ /** The latest date that can be chosen, `YYYY-MM-DD`, inclusive. */
40
+ get max() {
41
+ return this.#max.value;
42
+ }
43
+ set max(v) {
44
+ if (v !== undefined && !isIsoDate(v))
45
+ throw new TypeError("max must be a date, like 2026-10-07");
46
+ this.#max.value = v;
47
+ }
48
+ /** How a date is written in the input: `"medium"` (default), `"short"` or `"iso"`. */
49
+ get format() {
50
+ return this.#format.value;
51
+ }
52
+ set format(v) {
53
+ if (v !== "medium" && v !== "short" && v !== "iso")
54
+ throw new TypeError('format must be "medium", "short" or "iso"');
55
+ this.#format.value = v;
56
+ }
57
+ /** Placeholder text. Default: "None" for a nullable field, otherwise a date hint. */
58
+ get placeholder() {
59
+ return this.#placeholder.value;
60
+ }
61
+ set placeholder(v) {
62
+ this.#placeholder.value = v;
63
+ }
64
+ /** Whether choosing a day closes the calendar. Default `true`. */
65
+ get dismissOnSelect() {
66
+ return this.#dismissOnSelect.value;
67
+ }
68
+ set dismissOnSelect(v) {
69
+ this.#dismissOnSelect.value = v;
70
+ }
23
71
  focus(options) {
24
72
  this.querySelector("input")?.focus(options);
25
73
  }
74
+ validation(_meta, value) {
75
+ if (typeof value !== "string" || !isIsoDate(value))
76
+ return undefined;
77
+ const min = this.#min.value;
78
+ const max = this.#max.value;
79
+ if (min !== undefined && max !== undefined && min > max)
80
+ return "The earliest date is after the latest date.";
81
+ if (min !== undefined && value < min)
82
+ return `Choose ${formatDate(min)} or later.`;
83
+ if (max !== undefined && value > max)
84
+ return `Choose ${formatDate(max)} or earlier.`;
85
+ return undefined;
86
+ }
26
87
  control(c) {
27
88
  const value = typeof c.value === "string" ? c.value : undefined;
28
89
  const readOnly = c.state.readOnly;
90
+ const disabled = !c.state.enabled;
29
91
  const open = this.#calendar.open.value;
30
- const text = this.#draft.value ?? (value === undefined ? "" : formatDate(value));
92
+ const text = this.#draft.value ?? (value === undefined ? "" : formatDate(value, this.#format.value));
93
+ const placeholder = this.#placeholder.value || (c.meta?.nullable ? "None" : "A date, like 7 Oct");
31
94
  const button = `${c.id}-calendar`;
32
95
  const close = (refocus) => {
33
96
  this.#calendar.open.value = false;
@@ -37,8 +100,8 @@ export class DateField extends BoundField {
37
100
  return html `<div class="aster-field aster-date" data-ring-owner style=${anchorStyle(c.id)}>
38
101
  <input id=${c.id} .value=${live(text)}
39
102
  aria-label=${this.labelled ? nothing : c.label}
40
- placeholder=${c.meta?.nullable ? "None" : "A date, like 7 Oct"}
41
- ?readonly=${readOnly}
103
+ placeholder=${placeholder}
104
+ ?readonly=${readOnly} ?disabled=${disabled}
42
105
  aria-required=${c.state.required ? "true" : nothing}
43
106
  aria-invalid=${c.invalid ? "true" : nothing}
44
107
  aria-describedby=${c.describedBy ?? nothing}
@@ -53,6 +116,7 @@ export class DateField extends BoundField {
53
116
  ? nothing
54
117
  : html `<button type="button" class="aster-date-button" id=${button} aria-label=${`Choose ${c.label}`}
55
118
  aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"} aria-controls=${`${c.id}-dialog`}
119
+ ?disabled=${disabled}
56
120
  @pointerdown=${(e) => e.currentTarget.focus()}
57
121
  @mousedown=${(e) => e.preventDefault()}
58
122
  @click=${() => (open ? close(false) : this.#open(value))}>
@@ -65,10 +129,14 @@ export class DateField extends BoundField {
65
129
  value,
66
130
  today: today(),
67
131
  label: `Choose ${c.label}`,
132
+ min: this.#min.value,
133
+ max: this.#max.value,
68
134
  choose: (date) => {
69
135
  this.write(date);
70
- close(false);
71
- this.querySelector("input")?.focus();
136
+ if (this.#dismissOnSelect.peek()) {
137
+ close(false);
138
+ this.querySelector("input")?.focus();
139
+ }
72
140
  },
73
141
  dismiss: close,
74
142
  })}`;
@@ -1,6 +1,14 @@
1
1
  export declare function iso(year: number, month: number, day: number): string | undefined;
2
- /** "7 Oct 2026" for `2026-10-07`; other text unchanged. Month names are ours, the same in every engine. */
3
- export declare function formatDate(value: string): string;
2
+ /** How a date is written in a field's input. */
3
+ export type DateFormat = "medium" | "short" | "iso";
4
+ /**
5
+ * A date as written: "7 Oct 2026" (`medium`, the default), "7 Oct 26"
6
+ * (`short`) or `2026-10-07` (`iso`). Other text unchanged. Month names are
7
+ * ours, the same in every engine.
8
+ */
9
+ export declare function formatDate(value: string, format?: DateFormat): string;
10
+ /** Whether text is a real `YYYY-MM-DD` calendar date. */
11
+ export declare function isIsoDate(text: string): boolean;
4
12
  /**
5
13
  * A typed date: `2026-10-07`, `7/10/2026` (day first), `7 Oct 2026`,
6
14
  * `Oct 7 2026`, or `7 Oct` (this year). `undefined` when it isn't one.
@@ -7,10 +7,30 @@ export function iso(year, month, day) {
7
7
  return undefined;
8
8
  return d.toISOString().slice(0, 10);
9
9
  }
10
- /** "7 Oct 2026" for `2026-10-07`; other text unchanged. Month names are ours, the same in every engine. */
11
- export function formatDate(value) {
10
+ /**
11
+ * A date as written: "7 Oct 2026" (`medium`, the default), "7 Oct 26"
12
+ * (`short`) or `2026-10-07` (`iso`). Other text unchanged. Month names are
13
+ * ours, the same in every engine.
14
+ */
15
+ export function formatDate(value, format = "medium") {
12
16
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
13
- return m ? `${Number(m[3])} ${SHORT[Number(m[2]) - 1]} ${m[1]}` : value;
17
+ if (!m)
18
+ return value;
19
+ switch (format) {
20
+ case "iso":
21
+ return value;
22
+ case "short":
23
+ return `${Number(m[3])} ${SHORT[Number(m[2]) - 1]} ${m[1].slice(2)}`;
24
+ case "medium":
25
+ return `${Number(m[3])} ${SHORT[Number(m[2]) - 1]} ${m[1]}`;
26
+ }
27
+ }
28
+ /** Whether text is a real `YYYY-MM-DD` calendar date. */
29
+ export function isIsoDate(text) {
30
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(text))
31
+ return false;
32
+ const [y, m, d] = text.split("-").map(Number);
33
+ return iso(y, m, d) === text;
14
34
  }
15
35
  /**
16
36
  * A typed date: `2026-10-07`, `7/10/2026` (day first), `7 Oct 2026`,
@@ -0,0 +1,4 @@
1
+ /** A header appears when the dialog has a title, unless `header` says otherwise. */
2
+ export declare function dialogHeaderShown(title: string | undefined, header: boolean | undefined): boolean;
3
+ /** The close control belongs to the header and is off by default, preserving today's dialogs. */
4
+ export declare function dialogCloseShown(header: boolean, showCloseButton: boolean | undefined): boolean;
@@ -0,0 +1,8 @@
1
+ /** A header appears when the dialog has a title, unless `header` says otherwise. */
2
+ export function dialogHeaderShown(title, header) {
3
+ return header ?? (title !== undefined && title !== "");
4
+ }
5
+ /** The close control belongs to the header and is off by default, preserving today's dialogs. */
6
+ export function dialogCloseShown(header, showCloseButton) {
7
+ return header && showCloseButton === true;
8
+ }