@zeyos/zx 4.3.4 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +438 -0
- package/README.md +49 -10
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +217 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +23 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +11 -3
- package/dist/types/components/card-view/record-card.d.ts +7 -1
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +149 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +75 -6
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/field/field.d.ts +24 -2
- package/dist/types/components/fieldset/fieldset.d.ts +19 -1
- package/dist/types/components/file-item/file-item.d.ts +262 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
- package/dist/types/components/launcher/launcher.d.ts +278 -26
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +37 -42
- package/dist/types/components/message/message.d.ts +126 -17
- package/dist/types/components/modal/modal.d.ts +61 -2
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +15 -3
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
- package/dist/types/components/tabbox/tabbox.d.ts +16 -0
- package/dist/types/components/table/table.d.ts +190 -1
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/icons.d.ts +37 -22
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +15 -1
- package/dist/types/index.d.ts +16 -3
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +1 -1
- package/dist/types/zeyos/query.d.ts +79 -0
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +100 -14
- package/dist/types/zeyos/view.d.ts +10 -0
- package/dist/zx-zeyos.esm.js +2206 -489
- package/dist/zx-zeyos.esm.min.js +56 -1
- package/dist/zx.css +3322 -724
- package/dist/zx.esm.js +12244 -5809
- package/dist/zx.esm.min.js +58 -3
- package/dist/zx.global.js +12246 -5811
- package/dist/zx.global.min.js +58 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1683 -146
- package/docs/llms.md +777 -110
- package/docs/llms.txt +14 -8
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +602 -0
- package/src/components/activity-list/activity-list.css +99 -0
- package/src/components/activity-list/activity-list.js +409 -0
- package/src/components/app-icon/app-icon.css +1 -1
- package/src/components/app-rail/app-rail.css +191 -22
- package/src/components/app-sidebar/app-sidebar.css +349 -24
- package/src/components/app-sidebar/app-sidebar.js +86 -11
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/button/button.css +74 -8
- package/src/components/button/button.js +44 -1
- package/src/components/calendar/calendar.css +46 -46
- package/src/components/calendar/calendar.js +33 -3
- package/src/components/card/card.css +2 -2
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +10 -7
- package/src/components/card-view/card-view.js +25 -9
- package/src/components/card-view/record-card.js +7 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +68 -0
- package/src/components/code-editor/code-editor.js +306 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +87 -5
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +8 -3
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.css +1 -1
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +174 -70
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +7 -5
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +68 -26
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +28 -5
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +505 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +205 -47
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/finder/finder.css +15 -13
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +247 -0
- package/src/components/kanban-view/kanban-view.css +226 -8
- package/src/components/kanban-view/kanban-view.js +255 -12
- package/src/components/launcher/launcher.css +107 -25
- package/src/components/launcher/launcher.js +557 -63
- package/src/components/list-toolbar/list-toolbar.css +194 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/menu-button/menu-button.css +87 -5
- package/src/components/menu-button/menu-button.js +86 -46
- package/src/components/message/message.css +38 -5
- package/src/components/message/message.js +230 -17
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +159 -3
- package/src/components/navigation-bar/navigation-bar.css +95 -11
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/number-field/field-adapter.js +1 -0
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +8 -6
- package/src/components/panel/panel.css +1 -1
- package/src/components/panel/panel.js +13 -2
- package/src/components/rating/rating.css +7 -0
- package/src/components/search/search.js +17 -5
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +6 -1
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +86 -47
- package/src/components/sheet/sheet.css +11 -8
- package/src/components/slider/slider.css +16 -2
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +20 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +208 -0
- package/src/components/stat-tile/stat-tile.js +601 -0
- package/src/components/tabbox/tabbox.css +32 -27
- package/src/components/tabbox/tabbox.js +17 -2
- package/src/components/table/table.css +155 -31
- package/src/components/table/table.js +386 -33
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +86 -6
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +10 -3
- package/src/components/tree/tree.css +25 -16
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/breakpoint.js +1 -1
- package/src/core/icons.js +259 -57
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +21 -2
- package/src/index.js +30 -3
- package/src/internal/app-rail.js +18 -1
- package/src/internal/menu-items.js +142 -0
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +67 -27
- package/src/zeyos/index.js +1 -1
- package/src/zeyos/query.js +153 -0
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +68 -5
- package/src/zeyos/view.js +6 -0
- package/styles/base.css +56 -0
- package/styles/icons.css +11 -1
- package/styles/print.css +192 -0
- package/styles/tokens/dark.css +113 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +100 -31
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +11 -1
- package/styles/tokens/density.css +0 -9
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { registerChecklistFieldAdapter } from './components/checklist/field-adapter.js';
|
|
7
|
+
import { registerCodeEditorFieldAdapter } from './components/code-editor/field-adapter.js';
|
|
7
8
|
import { registerDatePickerFieldAdapters } from './components/date-picker/field-adapter.js';
|
|
8
9
|
import { registerDateRangeFieldAdapter } from './components/date-picker/range-field-adapter.js';
|
|
9
10
|
import { registerDateboxFieldAdapters } from './components/datebox/field-adapter.js';
|
|
@@ -25,6 +26,7 @@ import { registerValueListFieldAdapter } from './components/value-list/field-ada
|
|
|
25
26
|
export function registerFieldAdapters() {
|
|
26
27
|
registerSelectFieldAdapter();
|
|
27
28
|
registerChecklistFieldAdapter();
|
|
29
|
+
registerCodeEditorFieldAdapter();
|
|
28
30
|
registerDateboxFieldAdapters();
|
|
29
31
|
registerDatePickerFieldAdapters();
|
|
30
32
|
registerDateRangeFieldAdapter();
|
|
@@ -50,6 +52,7 @@ export {
|
|
|
50
52
|
} from './core/dom.js';
|
|
51
53
|
export { position } from './core/position.js';
|
|
52
54
|
export { breakpoints, breakpointOf, matchBreakpoint, onBreakpoint } from './core/breakpoint.js';
|
|
55
|
+
export { MOTION_ATTRIBUTE, prefersReducedMotion, watchMotion } from './core/motion.js';
|
|
53
56
|
export { Http, zeyosService, parseResult } from './core/http.js';
|
|
54
57
|
export { setTranslator, setLanguage, getLanguage, translate, printf } from './core/i18n.js';
|
|
55
58
|
export { focusTrap, rovingTabindex, typeahead } from './core/keyboard.js';
|
|
@@ -73,8 +76,8 @@ export {
|
|
|
73
76
|
export { storage } from './core/storage.js';
|
|
74
77
|
export { toCsv, downloadBlob, copyToClipboard } from './core/export.js';
|
|
75
78
|
export { defineElements } from './elements/define.js';
|
|
76
|
-
export { button, buttonGroup } from './components/button/button.js';
|
|
77
|
-
export { badge, badgeGroup } from './components/badge/badge.js';
|
|
79
|
+
export { button, buttonGroup, setButtonBusy } from './components/button/button.js';
|
|
80
|
+
export { BADGE_TONES, badge, badgeGroup } from './components/badge/badge.js';
|
|
78
81
|
export { CheckButton } from './components/check-button/check-button.js';
|
|
79
82
|
export { emptyState } from './components/empty-state/empty-state.js';
|
|
80
83
|
export { spinner } from './components/loading/spinner.js';
|
|
@@ -85,10 +88,12 @@ export { stack, grid, aspect } from './components/layout/layout.js';
|
|
|
85
88
|
export { truncate, isTruncated } from './components/truncate/truncate.js';
|
|
86
89
|
export { copyButton } from './components/copy/copy-button.js';
|
|
87
90
|
export { CopyInput } from './components/copy/copy-input.js';
|
|
91
|
+
export { CodeEditor, indentSelection, editorPosition } from './components/code-editor/code-editor.js';
|
|
88
92
|
export { Toolbar } from './components/toolbar/toolbar.js';
|
|
89
93
|
export { Toggle } from './components/toggle/toggle.js';
|
|
90
94
|
export { Groupbox } from './components/groupbox/groupbox.js';
|
|
91
95
|
export { Card } from './components/card/card.js';
|
|
96
|
+
export { StatTile, statTile } from './components/stat-tile/stat-tile.js';
|
|
92
97
|
export { Aurora } from './components/aurora/aurora.js';
|
|
93
98
|
export { Panel } from './components/panel/panel.js';
|
|
94
99
|
export { MasterPanel } from './components/master-panel/master-panel.js';
|
|
@@ -104,7 +109,19 @@ export { Pagination, paginationRange } from './components/pagination/pagination.
|
|
|
104
109
|
export { Search } from './components/search/search.js';
|
|
105
110
|
export { AppIcon, appIcon } from './components/app-icon/app-icon.js';
|
|
106
111
|
export { Launcher, rankLauncherItems } from './components/launcher/launcher.js';
|
|
107
|
-
export { Avatar, avatarInitials } from './components/avatar/avatar.js';
|
|
112
|
+
export { AVATAR_TONES, Avatar, avatarInitials, avatarTone } from './components/avatar/avatar.js';
|
|
113
|
+
export {
|
|
114
|
+
EntityRef, normalizeEntityRef, normalizeEntityRefLink,
|
|
115
|
+
normalizeEntityRefMetadata, normalizeEntityRefActions
|
|
116
|
+
} from './components/entity-ref/entity-ref.js';
|
|
117
|
+
export { FileItem, normalizeFileItem } from './components/file-item/file-item.js';
|
|
118
|
+
export { FileList, normalizeFileListItems } from './components/file-list/file-list.js';
|
|
119
|
+
export {
|
|
120
|
+
ActivityItem, resolveActivityActions, normalizeActivityAction, normalizeActivityTimestamp
|
|
121
|
+
} from './components/activity-item/activity-item.js';
|
|
122
|
+
export {
|
|
123
|
+
ActivityList, groupActivityItems, normalizeActivityItems
|
|
124
|
+
} from './components/activity-list/activity-list.js';
|
|
108
125
|
export { AccountMenu } from './components/account-menu/account-menu.js';
|
|
109
126
|
export { normalizeAppItems } from './internal/app-rail.js';
|
|
110
127
|
export { AppSidebar } from './components/app-sidebar/app-sidebar.js';
|
|
@@ -118,6 +135,7 @@ export { Tooltip, tooltip, describe } from './components/tooltip/tooltip.js';
|
|
|
118
135
|
export { MenuButton } from './components/menu-button/menu-button.js';
|
|
119
136
|
export { ContextMenu } from './components/context-menu/context-menu.js';
|
|
120
137
|
export { Select } from './components/select/select.js';
|
|
138
|
+
export { statusIcon, priorityIcon } from './components/select/glyphs.js';
|
|
121
139
|
export { Checklist } from './components/checklist/checklist.js';
|
|
122
140
|
export { TagPicker } from './components/tag-picker/tag-picker.js';
|
|
123
141
|
export { NumberField, parseNumber, snapNumber } from './components/number-field/number-field.js';
|
|
@@ -154,6 +172,10 @@ export {
|
|
|
154
172
|
kanbanSearchTerms, locateKanbanRecord, matchesKanbanSearch, normalizeKanbanRules,
|
|
155
173
|
reorderKanbanRecords, resolveKanbanRules
|
|
156
174
|
} from './components/kanban-view/kanban-policy.js';
|
|
175
|
+
export {
|
|
176
|
+
createKanbanAvatars, createKanbanCardHead, createKanbanIndicator, createKanbanProgress,
|
|
177
|
+
kanbanAvatarLimit, resolveKanbanAssignees, resolveKanbanIndicator, resolveKanbanProgress
|
|
178
|
+
} from './components/kanban-view/kanban-card.js';
|
|
157
179
|
export { Grid, billingColumns, isBillingLine } from './components/grid/grid.js';
|
|
158
180
|
export {
|
|
159
181
|
Chart, ChartJsAdapter, chartSummaryTable, cloneChartValue, isEmptyChartData
|
|
@@ -161,6 +183,11 @@ export {
|
|
|
161
183
|
export { TreeView } from './components/tree/tree.js';
|
|
162
184
|
export { Finder } from './components/finder/finder.js';
|
|
163
185
|
export { DataFilter } from './components/data-filter/data-filter.js';
|
|
186
|
+
export {
|
|
187
|
+
FilterPanel, isActiveValue, pruneValue, serializeFilterValue
|
|
188
|
+
} from './components/filter-panel/filter-panel.js';
|
|
189
|
+
export { SortControl } from './components/sort-control/sort-control.js';
|
|
190
|
+
export { ListToolbar } from './components/list-toolbar/list-toolbar.js';
|
|
164
191
|
export { Filter } from './components/filter/filter.js';
|
|
165
192
|
export {
|
|
166
193
|
filterOperators, emptyFilterAst, filterCondition, filterGroup,
|
package/src/internal/app-rail.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component } from '../core/component.js';
|
|
2
|
+
import { printf } from '../core/i18n.js';
|
|
2
3
|
import { h, safeHref } from '../core/dom.js';
|
|
3
4
|
import { icon } from '../core/icons.js';
|
|
4
5
|
import { Dropdown } from '../components/dropdown/dropdown.js';
|
|
@@ -292,8 +293,11 @@ export class AppRailPresenter extends Component {
|
|
|
292
293
|
});
|
|
293
294
|
const actualPanel = dropdown.getPanel();
|
|
294
295
|
actualPanel.classList.add('zx-app-rail__popover');
|
|
296
|
+
// The panel is portaled out of the rail, so its stylesheet cannot ask which edge the rail
|
|
297
|
+
// occupies. Presentation only: it tells the panel's styles which side it opened from.
|
|
298
|
+
actualPanel.dataset.side = this.el.dataset.side;
|
|
295
299
|
actualPanel.setAttribute('role', 'region');
|
|
296
|
-
actualPanel.setAttribute('aria-label',
|
|
300
|
+
actualPanel.setAttribute('aria-label', this._message('appRail.subNavigation', '%1 sub-navigation', item.label));
|
|
297
301
|
const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
|
|
298
302
|
this._flyouts.set(key, flyout);
|
|
299
303
|
dropdown.on('open', () => {
|
|
@@ -573,6 +577,19 @@ export class AppRailPresenter extends Component {
|
|
|
573
577
|
this._interaction = null;
|
|
574
578
|
this._flyouts = new Map();
|
|
575
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Resolves one message through `msg()`, which falls back to the host translator. Copied from
|
|
582
|
+
* `Pagination` rather than invented: the rail's flyout label was the last user-visible string
|
|
583
|
+
* in the navigation stack with no way to change it.
|
|
584
|
+
* @param {string} key Message key.
|
|
585
|
+
* @param {string} fallback English default.
|
|
586
|
+
* @param {...unknown} args Interpolation arguments.
|
|
587
|
+
* @returns {string}
|
|
588
|
+
*/
|
|
589
|
+
_message(key, fallback, ...args) {
|
|
590
|
+
const message = this.msg(key, ...args);
|
|
591
|
+
return message === key ? printf(fallback, args) : message;
|
|
592
|
+
}
|
|
576
593
|
}
|
|
577
594
|
|
|
578
595
|
/** Owns listeners attached to one dynamic rail render so `setItems()` can release them immediately. */
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { h, safeHref } from '../core/dom.js';
|
|
2
|
+
import { icon } from '../core/icons.js';
|
|
3
|
+
|
|
4
|
+
/** @typedef {'menuitem'|'menuitemcheckbox'|'menuitemradio'} MenuItemRole */
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} MenuController
|
|
7
|
+
* @property {() => unknown} close Close the owning menu.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} MenuItem
|
|
11
|
+
* @property {string} label Item label.
|
|
12
|
+
* @property {string} [icon] Kernel icon name.
|
|
13
|
+
* @property {unknown} [value] Value emitted on selection.
|
|
14
|
+
* @property {boolean} [disabled=false] Whether selection is disabled.
|
|
15
|
+
* @property {boolean} [danger=false] Whether the action is dangerous.
|
|
16
|
+
* @property {string} [href] Native-link destination.
|
|
17
|
+
* @property {string} [target] Native-link target.
|
|
18
|
+
* @property {string} [rel] Native-link relationship.
|
|
19
|
+
* @property {string} [description] Supporting text under the label.
|
|
20
|
+
* @property {string|number} [badge] Compact trailing metadata.
|
|
21
|
+
* @property {string} [shortcut] Keyboard shortcut label.
|
|
22
|
+
* @property {string|number|Node} [adornment] Additional trailing content.
|
|
23
|
+
* @property {MenuItemRole} [role='menuitem'] APG role.
|
|
24
|
+
* @property {boolean} [checked=false] Checked state for checkbox/radio roles.
|
|
25
|
+
* @property {(value: unknown, item: MenuItem, controller: MenuController) => void} [onselect]
|
|
26
|
+
* Selection callback.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} MenuHeading
|
|
30
|
+
* @property {'heading'} type Entry kind.
|
|
31
|
+
* @property {string} label Visible heading.
|
|
32
|
+
*/
|
|
33
|
+
/** @typedef {{type: 'separator'}} MenuSeparator */
|
|
34
|
+
/** @typedef {MenuItem|MenuHeading|MenuSeparator|'-'} MenuEntry */
|
|
35
|
+
|
|
36
|
+
export const MENU_ITEM_SELECTOR = [
|
|
37
|
+
'[role="menuitem"]',
|
|
38
|
+
'[role="menuitemcheckbox"]',
|
|
39
|
+
'[role="menuitemradio"]'
|
|
40
|
+
].join(',');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reports whether an entry is an interactive menu action.
|
|
44
|
+
* @param {MenuEntry|unknown} entry Candidate entry.
|
|
45
|
+
* @returns {entry is MenuItem}
|
|
46
|
+
*/
|
|
47
|
+
export function isMenuItem(entry) {
|
|
48
|
+
return Boolean(entry && entry !== '-' && typeof entry === 'object'
|
|
49
|
+
&& !['heading', 'separator'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the only APG roles accepted by the shared action model.
|
|
54
|
+
* @param {MenuItem} item Item definition.
|
|
55
|
+
* @returns {MenuItemRole}
|
|
56
|
+
*/
|
|
57
|
+
export function menuItemRole(item) {
|
|
58
|
+
return ['menuitemcheckbox', 'menuitemradio'].includes(String(item.role))
|
|
59
|
+
? /** @type {MenuItemRole} */ (item.role)
|
|
60
|
+
: 'menuitem';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Builds one menu's entries with shared structure and link safety.
|
|
65
|
+
* @param {MenuEntry[]} entries Entry definitions.
|
|
66
|
+
* @param {string} block CSS block name, without the `zx-` prefix.
|
|
67
|
+
* @returns {HTMLElement[]}
|
|
68
|
+
*/
|
|
69
|
+
export function renderMenuEntries(entries, block) {
|
|
70
|
+
const nodes = [];
|
|
71
|
+
entries.forEach((entry, index) => {
|
|
72
|
+
if (entry === '-' || (entry && typeof entry === 'object' && entry.type === 'separator')) {
|
|
73
|
+
nodes.push(h('div', { class: `zx-${block}__separator`, role: 'separator' }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (entry && typeof entry === 'object' && entry.type === 'heading') {
|
|
77
|
+
nodes.push(h('div', {
|
|
78
|
+
class: `zx-${block}__heading`,
|
|
79
|
+
role: 'presentation'
|
|
80
|
+
}, String(entry.label ?? '')));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isMenuItem(entry)) return;
|
|
84
|
+
|
|
85
|
+
const role = menuItemRole(entry);
|
|
86
|
+
const href = safeHref(entry.href);
|
|
87
|
+
const target = entry.target == null ? null : String(entry.target);
|
|
88
|
+
const rel = entry.rel == null
|
|
89
|
+
? (target === '_blank' ? 'noopener noreferrer' : null)
|
|
90
|
+
: String(entry.rel);
|
|
91
|
+
const content = [];
|
|
92
|
+
const leading = [];
|
|
93
|
+
if (role !== 'menuitem') {
|
|
94
|
+
leading.push(h('span', {
|
|
95
|
+
class: `zx-${block}__check`,
|
|
96
|
+
'data-check-kind': role === 'menuitemradio' ? 'radio' : 'checkbox'
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
if (entry.icon) leading.push(icon(entry.icon));
|
|
100
|
+
content.push(h('span', {
|
|
101
|
+
class: `zx-${block}__leading`,
|
|
102
|
+
ariaHidden: 'true'
|
|
103
|
+
}, leading));
|
|
104
|
+
content.push(h('span', { class: `zx-${block}__body` },
|
|
105
|
+
h('span', { class: `zx-${block}__label` }, String(entry.label ?? '')),
|
|
106
|
+
entry.description == null || entry.description === '' ? null : h('span', {
|
|
107
|
+
class: `zx-${block}__description`
|
|
108
|
+
}, String(entry.description))));
|
|
109
|
+
|
|
110
|
+
const meta = [];
|
|
111
|
+
if (entry.badge != null) {
|
|
112
|
+
meta.push(h('span', { class: `zx-${block}__badge` }, String(entry.badge)));
|
|
113
|
+
}
|
|
114
|
+
if (entry.shortcut) {
|
|
115
|
+
meta.push(h('kbd', { class: `zx-${block}__shortcut` }, String(entry.shortcut)));
|
|
116
|
+
}
|
|
117
|
+
if (entry.adornment != null) {
|
|
118
|
+
const adornment = entry.adornment && typeof entry.adornment === 'object'
|
|
119
|
+
&& 'nodeType' in entry.adornment
|
|
120
|
+
? /** @type {Node} */ (entry.adornment)
|
|
121
|
+
: String(entry.adornment);
|
|
122
|
+
meta.push(h('span', { class: `zx-${block}__adornment` }, adornment));
|
|
123
|
+
}
|
|
124
|
+
if (meta.length) content.push(h('span', { class: `zx-${block}__meta` }, meta));
|
|
125
|
+
|
|
126
|
+
const properties = {
|
|
127
|
+
class: `zx-${block}__item`,
|
|
128
|
+
role,
|
|
129
|
+
tabindex: '-1',
|
|
130
|
+
'data-menu-item': String(index),
|
|
131
|
+
'data-danger': entry.danger ? 'true' : null,
|
|
132
|
+
ariaDisabled: entry.disabled ? 'true' : null,
|
|
133
|
+
ariaChecked: role === 'menuitem' ? null : String(Boolean(entry.checked))
|
|
134
|
+
};
|
|
135
|
+
if (href) {
|
|
136
|
+
nodes.push(h('a', { ...properties, href, target, rel }, content));
|
|
137
|
+
} else {
|
|
138
|
+
nodes.push(h('button', { ...properties, type: 'button' }, content));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return nodes;
|
|
142
|
+
}
|
package/src/zeyos/connect.js
CHANGED
|
@@ -107,8 +107,12 @@ function isRecord(value) {
|
|
|
107
107
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The text a failed request is reported with: the error's own message, or a generic sentence.
|
|
112
|
+
* @param {unknown} error Rejection reason.
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function errorMessage(error) {
|
|
112
116
|
if (error instanceof Error && error.message) return error.message;
|
|
113
117
|
if (error && typeof error === 'object' && typeof error.message === 'string') return error.message;
|
|
114
118
|
return String(error ?? 'ZeyOS request failed');
|
package/src/zeyos/form.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Form, Message, Select } from '../index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { overlayHost } from '../core/overlay-host.js';
|
|
3
|
+
import { connect, errorMessage } from './connect.js';
|
|
4
|
+
import {
|
|
5
|
+
calendarDayToUnixSeconds, dateToUnixSeconds, unixSecondsToCalendarDay, unixSecondsToDate
|
|
6
|
+
} from './query.js';
|
|
4
7
|
import { fieldToZxField, resolveFields } from './schema.js';
|
|
5
8
|
import { buildZeyosSelectConfig } from './select.js';
|
|
6
9
|
|
|
@@ -9,11 +12,21 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
9
12
|
* @property {string[]} [fields] Curated ordered field allow-list.
|
|
10
13
|
* @property {string[]} [exclude] Field ids to omit.
|
|
11
14
|
* @property {Record<string, string>} [labels] Per-field label overrides.
|
|
15
|
+
* @property {Record<string, string>} [formats] Per-field semantic format overrides, such as
|
|
16
|
+
* `{ duedate: 'date' }` for a Unix-second field that holds a calendar day.
|
|
17
|
+
* @property {string} [timeZone] IANA time zone of the ZeyOS instance. A date-only field's stored
|
|
18
|
+
* midnight is read as the calendar day it names in this zone, and a picked day is saved as midnight
|
|
19
|
+
* in it; defaults to the runtime's own zone.
|
|
12
20
|
* @property {string} [title=''] Fieldset legend.
|
|
13
21
|
* @property {1|2|3} [columns=2] Fieldset column count.
|
|
14
22
|
* @property {Record<string, unknown>} [value] Initial record using ZeyOS storage values.
|
|
15
23
|
* @property {unknown} [id] Initial record id to load.
|
|
16
24
|
* @property {(record: Record<string, unknown>) => unknown|Promise<unknown>} [onSaved] Success callback.
|
|
25
|
+
* @property {Element|string|null} [scope=null] Element or selector whose theme scope shows the
|
|
26
|
+
* binding's toasts — the save confirmation and the report of a failed request. Null uses the form's
|
|
27
|
+
* own element, so a form inside a scoped shell or dialog toasts inside that scope.
|
|
28
|
+
* @property {boolean} [notify=true] Whether the binding raises toasts. `false` keeps it silent:
|
|
29
|
+
* `save()`, `load()`, and `ready` still resolve or reject, and reporting is the caller's.
|
|
17
30
|
*/
|
|
18
31
|
|
|
19
32
|
/**
|
|
@@ -22,8 +35,10 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
22
35
|
* @property {Record<string, Record<string, any>>} fields Field descriptors keyed by id.
|
|
23
36
|
* @property {Array<{id: string, resource: string}>} entityFields Referenced-resource fields.
|
|
24
37
|
* @property {Record<string, any>} formOptions Plain Zx Form options.
|
|
25
|
-
* @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix
|
|
26
|
-
*
|
|
38
|
+
* @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix
|
|
39
|
+
* seconds to Date values: a date-only field to local midnight of its calendar day in `timeZone`.
|
|
40
|
+
* @property {(values: Record<string, unknown>) => Record<string, unknown>} mapToRecord Maps Date and
|
|
41
|
+
* enum values to API storage values: a date-only field to midnight of its day in `timeZone`.
|
|
27
42
|
*/
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -31,7 +46,8 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
31
46
|
* @property {Form} form Generated Zx form.
|
|
32
47
|
* @property {Promise<Record<string, unknown>|null>} ready Initial value/load readiness.
|
|
33
48
|
* @property {(id: unknown) => Promise<Record<string, unknown>>} load Loads and maps one record.
|
|
34
|
-
* @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the
|
|
49
|
+
* @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the
|
|
50
|
+
* record, then confirms with a toast in the form's scope unless `notify` is false.
|
|
35
51
|
* @property {() => Form} getForm Returns the generated Zx Form.
|
|
36
52
|
* @property {() => void} destroy Destroys the form and all owned fields/selects.
|
|
37
53
|
*/
|
|
@@ -39,7 +55,8 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
39
55
|
/**
|
|
40
56
|
* Builds the DOM-free form configuration consumed by {@link zeyosForm}. Entity descriptors retain
|
|
41
57
|
* the `zxselect` type and receive the same async options used by {@link zeyosSelect}; enum options
|
|
42
|
-
* come from normalized schema metadata, and the returned mappers handle
|
|
58
|
+
* come from normalized schema metadata with readable labels, and the returned mappers handle
|
|
59
|
+
* Unix-second dates — date-only fields as calendar days in `opts.timeZone`.
|
|
43
60
|
* @param {Record<string, any>} client Injected ZeyOS client instance.
|
|
44
61
|
* @param {string} resource Resource name such as `transactions`.
|
|
45
62
|
* @param {ZeyosFormOptions} [opts={}] Schema curation and layout options.
|
|
@@ -49,9 +66,14 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
|
|
|
49
66
|
const fieldMeta = resolveFields(client, resource, opts);
|
|
50
67
|
const fields = {};
|
|
51
68
|
const entityFields = [];
|
|
69
|
+
/** @type {Map<string, 'date'|'datetime'>} */
|
|
70
|
+
const dateKinds = new Map();
|
|
52
71
|
|
|
53
72
|
for (const meta of fieldMeta) {
|
|
54
73
|
const descriptor = fieldToZxField(meta);
|
|
74
|
+
if (descriptor.type === 'date' || descriptor.type === 'datetime') {
|
|
75
|
+
dateKinds.set(String(meta.id), descriptor.type);
|
|
76
|
+
}
|
|
55
77
|
const referencedResource = descriptor.type === 'zxselect' && typeof descriptor.props?.entity === 'string'
|
|
56
78
|
? descriptor.props.entity
|
|
57
79
|
: null;
|
|
@@ -78,16 +100,18 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
|
|
|
78
100
|
fields
|
|
79
101
|
}]
|
|
80
102
|
},
|
|
81
|
-
mapFromRecord: (record) => mapFromRecord(record, fieldMeta),
|
|
82
|
-
mapToRecord: (values) => mapToRecord(values, fieldMeta)
|
|
103
|
+
mapFromRecord: (record) => mapFromRecord(record, fieldMeta, dateKinds),
|
|
104
|
+
mapToRecord: (values) => mapToRecord(values, fieldMeta, dateKinds)
|
|
83
105
|
};
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
87
109
|
* Generates a schema-driven Zx Form and a small async binding object. `load(id)` hydrates entity
|
|
88
110
|
* labels and converts Unix seconds to Date objects. `save()` validates, maps values back to API
|
|
89
|
-
* storage types, calls create/update through the WP14 connection facade,
|
|
90
|
-
*
|
|
111
|
+
* storage types, calls create/update through the WP14 connection facade, invokes
|
|
112
|
+
* `onSaved(record)`, and shows a success toast. Its toasts — that confirmation and the report of a
|
|
113
|
+
* failed request — appear in the theme scope of `opts.scope` or of the form itself, and
|
|
114
|
+
* `notify: false` silences them. Await `ready` when `opts.id` or `opts.value` is supplied.
|
|
91
115
|
* @param {Record<string, any>} client Injected ZeyOS client instance.
|
|
92
116
|
* @param {string} resource Resource name such as `transactions`.
|
|
93
117
|
* @param {ZeyosFormOptions} [opts={}] Schema, initial value, and save options.
|
|
@@ -97,8 +121,14 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
97
121
|
if (opts.onSaved != null && typeof opts.onSaved !== 'function') {
|
|
98
122
|
throw new TypeError('zeyosForm onSaved must be a function');
|
|
99
123
|
}
|
|
124
|
+
const notify = opts.notify !== false;
|
|
100
125
|
const config = buildZeyosFormConfig(client, resource, opts);
|
|
101
|
-
|
|
126
|
+
// The caller's scope, or the form's own element, decides where a toast appears — never whatever
|
|
127
|
+
// element happens to hold focus when a request settles.
|
|
128
|
+
const toastScope = () => opts.scope ?? form.el;
|
|
129
|
+
const facade = connect(client, {
|
|
130
|
+
onError: (error) => notify ? Message.error(errorMessage(error), { scope: toastScope() }) : undefined
|
|
131
|
+
});
|
|
102
132
|
const form = new Form(null, config.formOptions);
|
|
103
133
|
let currentId = opts.value?.ID ?? opts.id ?? null;
|
|
104
134
|
let destroyed = false;
|
|
@@ -133,6 +163,8 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
133
163
|
if (destroyed) return Promise.reject(new Error('Cannot save a destroyed ZeyOS form'));
|
|
134
164
|
if (savePending) return savePending;
|
|
135
165
|
if (!form.submit()) return Promise.resolve(null);
|
|
166
|
+
// Resolved now, while the form is in place: `onSaved` may close the dialog that holds it.
|
|
167
|
+
const host = notify ? overlayHost(toastScope()) : null;
|
|
136
168
|
|
|
137
169
|
savePending = (async () => {
|
|
138
170
|
let payload;
|
|
@@ -157,7 +189,7 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
157
189
|
throw error;
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
|
-
Message.success(`${recordLabel(record, resource)} saved successfully
|
|
192
|
+
if (host) Message.success(`${recordLabel(record, resource)} saved successfully.`, { scope: host });
|
|
161
193
|
return record;
|
|
162
194
|
})().finally(() => {
|
|
163
195
|
savePending = null;
|
|
@@ -183,27 +215,41 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
183
215
|
};
|
|
184
216
|
}
|
|
185
217
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
218
|
+
/**
|
|
219
|
+
* @param {Record<string, unknown>} record
|
|
220
|
+
* @param {Record<string, any>[]} meta
|
|
221
|
+
* @param {Map<string, 'date'|'datetime'>} dateKinds
|
|
222
|
+
* @returns {Record<string, unknown>}
|
|
223
|
+
*/
|
|
224
|
+
function mapFromRecord(record, meta, dateKinds) {
|
|
188
225
|
const values = {};
|
|
189
226
|
for (const field of meta) {
|
|
190
227
|
const id = String(field.id);
|
|
191
228
|
const value = record[id];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
229
|
+
const kind = dateKinds.get(id);
|
|
230
|
+
if (!kind || value == null || value === '') values[id] = value;
|
|
231
|
+
else if (kind === 'date') values[id] = unixSecondsToCalendarDay(/** @type {number|string} */ (value), field.timeZone);
|
|
232
|
+
else values[id] = unixSecondsToDate(/** @type {number|string} */ (value));
|
|
195
233
|
}
|
|
196
234
|
return values;
|
|
197
235
|
}
|
|
198
236
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
237
|
+
/**
|
|
238
|
+
* @param {Record<string, unknown>} values
|
|
239
|
+
* @param {Record<string, any>[]} meta
|
|
240
|
+
* @param {Map<string, 'date'|'datetime'>} dateKinds
|
|
241
|
+
* @returns {Record<string, unknown>}
|
|
242
|
+
*/
|
|
243
|
+
function mapToRecord(values, meta, dateKinds) {
|
|
201
244
|
const record = {};
|
|
202
245
|
for (const field of meta) {
|
|
203
246
|
const id = String(field.id);
|
|
204
247
|
let value = values[id];
|
|
205
|
-
|
|
206
|
-
|
|
248
|
+
const kind = dateKinds.get(id);
|
|
249
|
+
if (kind) {
|
|
250
|
+
if (value == null || value === '') value = null;
|
|
251
|
+
else if (kind === 'date') value = calendarDayToUnixSeconds(/** @type {Date} */ (value), field.timeZone);
|
|
252
|
+
else value = dateToUnixSeconds(/** @type {Date} */ (value));
|
|
207
253
|
} else if (hasEnum(field) && isNumericType(field.type) && value !== '' && value != null) {
|
|
208
254
|
const numeric = Number(value);
|
|
209
255
|
if (Number.isFinite(numeric)) value = numeric;
|
|
@@ -213,12 +259,6 @@ function mapToRecord(values, meta) {
|
|
|
213
259
|
return record;
|
|
214
260
|
}
|
|
215
261
|
|
|
216
|
-
/** @param {Record<string, any>} meta @returns {boolean} */
|
|
217
|
-
function isDateField(meta) {
|
|
218
|
-
const descriptor = fieldToZxField(meta);
|
|
219
|
-
return descriptor.type === 'date' || descriptor.type === 'datetime';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
262
|
/** @param {Record<string, any>} meta @returns {boolean} */
|
|
223
263
|
function hasEnum(meta) {
|
|
224
264
|
return meta.enums != null || meta.enum != null;
|
package/src/zeyos/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
} from './schema.js';
|
|
9
9
|
export {
|
|
10
10
|
buildListQuery, tableSortToQuery, dataFilterStateToFilters,
|
|
11
|
-
dateToUnixSeconds, unixSecondsToDate
|
|
11
|
+
dateToUnixSeconds, unixSecondsToDate, calendarDayToUnixSeconds, unixSecondsToCalendarDay
|
|
12
12
|
} from './query.js';
|
|
13
13
|
export { connect } from './connect.js';
|
|
14
14
|
export {
|
package/src/zeyos/query.js
CHANGED
|
@@ -55,6 +55,159 @@ export function unixSecondsToDate(seconds) {
|
|
|
55
55
|
return new Date(value * 1000);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} CalendarDay
|
|
60
|
+
* @property {number} year Full year.
|
|
61
|
+
* @property {number} month Zero-based month, as `Date` counts them.
|
|
62
|
+
* @property {number} day Day of the month.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} CalendarZone
|
|
67
|
+
* @property {string|undefined} timeZone Canonical IANA zone, or undefined for the runtime's own zone.
|
|
68
|
+
* @property {(milliseconds: number) => CalendarDay} dayOf Calendar day an instant falls on in the zone.
|
|
69
|
+
* @property {(year: number, month: number, day: number) => number} midnightOf Epoch milliseconds of
|
|
70
|
+
* the first moment of a calendar day in the zone (its midnight, or the first minute after a
|
|
71
|
+
* daylight-saving gap that swallows midnight).
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reads a ZeyOS date-only value as the calendar day it names. Date-only fields — an invoice's
|
|
76
|
+
* `date` and `duedate`, a contact's `birthdate` — hold Unix seconds at midnight of a day in the
|
|
77
|
+
* instance's time zone: a calendar day, not an instant. The result is midnight of that same day in
|
|
78
|
+
* the runtime's own zone, which is what Datebox and DatePicker edit, so the day survives a viewer in
|
|
79
|
+
* another zone and a daylight-saving change.
|
|
80
|
+
* @param {number|string|null|undefined} seconds Unix seconds; numeric strings are accepted.
|
|
81
|
+
* @param {string} [timeZone] IANA zone of the instance, such as `Europe/Vienna`; the runtime's own
|
|
82
|
+
* zone when omitted.
|
|
83
|
+
* @returns {Date|null} Local midnight of the day, or null for a nullish or empty input.
|
|
84
|
+
*/
|
|
85
|
+
export function unixSecondsToCalendarDay(seconds, timeZone) {
|
|
86
|
+
if (seconds == null || seconds === '') return null;
|
|
87
|
+
const value = Number(seconds);
|
|
88
|
+
if (!Number.isFinite(value)) throw new TypeError('Expected finite Unix seconds');
|
|
89
|
+
const { year, month, day } = calendarZone(timeZone).dayOf(value * 1000);
|
|
90
|
+
return localMidnight(year, month, day);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Converts the calendar day of a Date to the Unix seconds a ZeyOS date-only field stores: midnight
|
|
95
|
+
* of that day in the instance's time zone. Only the Date's local year, month, and day count — its
|
|
96
|
+
* time of day is ignored — so a Datebox value round-trips through {@link unixSecondsToCalendarDay}.
|
|
97
|
+
* @param {Date|null|undefined} date Day to store.
|
|
98
|
+
* @param {string} [timeZone] IANA zone of the instance; the runtime's own zone when omitted.
|
|
99
|
+
* @returns {number|null} Unix seconds, or null for a nullish input.
|
|
100
|
+
*/
|
|
101
|
+
export function calendarDayToUnixSeconds(date, timeZone) {
|
|
102
|
+
if (date == null) return null;
|
|
103
|
+
if (!(date instanceof Date)) throw new TypeError('Expected a Date');
|
|
104
|
+
if (!Number.isFinite(date.getTime())) throw new RangeError('Invalid Date');
|
|
105
|
+
const midnight = calendarZone(timeZone).midnightOf(date.getFullYear(), date.getMonth(), date.getDate());
|
|
106
|
+
return Math.floor(midnight / 1000);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validates an IANA time-zone name.
|
|
111
|
+
* @param {unknown} timeZone Zone name; nullish or empty selects the runtime's own zone.
|
|
112
|
+
* @returns {string|undefined} The canonical zone name, or undefined for the runtime's own zone.
|
|
113
|
+
* @throws {RangeError} When the runtime does not know the zone.
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeTimeZone(timeZone) {
|
|
116
|
+
if (timeZone == null || timeZone === '') return undefined;
|
|
117
|
+
try {
|
|
118
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: String(timeZone) }).resolvedOptions().timeZone;
|
|
119
|
+
} catch {
|
|
120
|
+
throw new RangeError(`Unknown time zone "${String(timeZone)}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Calendar arithmetic for one time zone. The `Intl` formatter is built once, so a table column can
|
|
126
|
+
* hold one zone for all of its cells.
|
|
127
|
+
* @param {string} [timeZone] IANA zone; the runtime's own zone when omitted.
|
|
128
|
+
* @returns {CalendarZone} Day lookup and midnight construction for the zone.
|
|
129
|
+
*/
|
|
130
|
+
export function calendarZone(timeZone) {
|
|
131
|
+
const zone = normalizeTimeZone(timeZone);
|
|
132
|
+
if (!zone) {
|
|
133
|
+
return {
|
|
134
|
+
timeZone: undefined,
|
|
135
|
+
dayOf: (milliseconds) => {
|
|
136
|
+
const date = new Date(milliseconds);
|
|
137
|
+
return { year: date.getFullYear(), month: date.getMonth(), day: date.getDate() };
|
|
138
|
+
},
|
|
139
|
+
midnightOf: (year, month, day) => localMidnight(year, month, day).getTime()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
144
|
+
timeZone: zone,
|
|
145
|
+
hourCycle: 'h23',
|
|
146
|
+
year: 'numeric',
|
|
147
|
+
month: 'numeric',
|
|
148
|
+
day: 'numeric',
|
|
149
|
+
hour: 'numeric',
|
|
150
|
+
minute: 'numeric',
|
|
151
|
+
second: 'numeric'
|
|
152
|
+
});
|
|
153
|
+
/** @param {number} milliseconds @returns {CalendarDay & {seconds: number}} */
|
|
154
|
+
const wallClock = (milliseconds) => {
|
|
155
|
+
const parts = {};
|
|
156
|
+
for (const part of formatter.formatToParts(new Date(milliseconds))) parts[part.type] = Number(part.value);
|
|
157
|
+
return {
|
|
158
|
+
year: parts.year,
|
|
159
|
+
month: parts.month - 1,
|
|
160
|
+
day: parts.day,
|
|
161
|
+
seconds: (parts.hour % 24) * 3600 + parts.minute * 60 + parts.second
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
/** @param {number} milliseconds @returns {number} The zone's UTC offset at that instant, in milliseconds. */
|
|
165
|
+
const offsetAt = (milliseconds) => {
|
|
166
|
+
const wall = wallClock(milliseconds);
|
|
167
|
+
return utcMidnight(wall.year, wall.month, wall.day) + wall.seconds * 1000
|
|
168
|
+
- Math.floor(milliseconds / 1000) * 1000;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
timeZone: zone,
|
|
173
|
+
dayOf: (milliseconds) => {
|
|
174
|
+
const { year, month, day } = wallClock(milliseconds);
|
|
175
|
+
return { year, month, day };
|
|
176
|
+
},
|
|
177
|
+
midnightOf: (year, month, day) => {
|
|
178
|
+
// Midnight is the instant whose wall clock reads the target day at 00:00. Two offset
|
|
179
|
+
// corrections settle it on either side of a daylight-saving change; where a change skips
|
|
180
|
+
// midnight altogether, the earlier candidate lands on the previous evening and the later one
|
|
181
|
+
// on the first minute of the day, which is the one kept.
|
|
182
|
+
const target = utcMidnight(year, month, day);
|
|
183
|
+
const first = target - offsetAt(target);
|
|
184
|
+
const second = target - offsetAt(first);
|
|
185
|
+
let best = null;
|
|
186
|
+
for (const candidate of [second, first]) {
|
|
187
|
+
const wall = wallClock(candidate);
|
|
188
|
+
if (wall.year !== year || wall.month !== month || wall.day !== day) continue;
|
|
189
|
+
if (!best || wall.seconds < best.seconds) best = { candidate, seconds: wall.seconds };
|
|
190
|
+
}
|
|
191
|
+
return best ? best.candidate : second;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @param {number} year @param {number} month @param {number} day @returns {Date} */
|
|
197
|
+
function localMidnight(year, month, day) {
|
|
198
|
+
const date = new Date(2000, 0, 1);
|
|
199
|
+
date.setFullYear(year, month, day);
|
|
200
|
+
date.setHours(0, 0, 0, 0);
|
|
201
|
+
return date;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @param {number} year @param {number} month @param {number} day @returns {number} */
|
|
205
|
+
function utcMidnight(year, month, day) {
|
|
206
|
+
const date = new Date(0);
|
|
207
|
+
date.setUTCFullYear(year, month, day);
|
|
208
|
+
return date.getTime();
|
|
209
|
+
}
|
|
210
|
+
|
|
58
211
|
/**
|
|
59
212
|
* Converts one or more Zx Table sort records to the signed field notation used by ZeyOS.
|
|
60
213
|
* A missing sort produces an empty array.
|