@zeyos/zx 5.0.0 → 5.2.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 +164 -0
- package/README.md +2 -1
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +9 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/dropdown/dropdown.d.ts +22 -7
- package/dist/types/components/field/field.d.ts +30 -0
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +7 -0
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/launcher/launcher.d.ts +8 -0
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
- package/dist/types/components/menu-button/menu-button.d.ts +6 -0
- package/dist/types/components/message/message.d.ts +14 -1
- package/dist/types/components/modal/modal.d.ts +30 -0
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/select.d.ts +6 -0
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/table/table.d.ts +265 -17
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/overlay-host.d.ts +28 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/index.d.ts +1 -0
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +31 -5
- package/dist/types/zeyos/table.d.ts +245 -8
- package/dist/types/zeyos/view.d.ts +16 -2
- package/dist/zx-zeyos.esm.js +4422 -430
- package/dist/zx-zeyos.esm.min.js +5 -5
- package/dist/zx.css +1417 -134
- package/dist/zx.esm.js +6986 -830
- package/dist/zx.esm.min.js +8 -8
- package/dist/zx.global.js +6986 -830
- package/dist/zx.global.min.js +8 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +794 -17
- package/docs/llms.md +503 -63
- package/docs/llms.txt +6 -2
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-list/activity-list.js +27 -1
- package/src/components/app-icon/app-icon.css +13 -2
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +19 -0
- package/src/components/app-sidebar/app-sidebar.css +204 -3
- package/src/components/app-sidebar/app-sidebar.js +646 -40
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/calendar/calendar.css +1 -1
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +5 -5
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/code-editor/code-editor.css +1 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-input.css +1 -1
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/time-picker.css +2 -2
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.js +157 -72
- package/src/components/field/field.js +33 -3
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.js +10 -59
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +300 -8
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +1 -1
- package/src/components/kanban-view/kanban-view.css +3 -3
- package/src/components/kanban-view/kanban-view.js +5 -2
- package/src/components/launcher/launcher.css +1 -1
- package/src/components/launcher/launcher.js +29 -3
- package/src/components/list-toolbar/list-toolbar.js +39 -13
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +63 -0
- package/src/components/menu-button/menu-button.js +97 -11
- package/src/components/message/message.js +60 -20
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +66 -13
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/number-field/field-adapter.js +2 -1
- package/src/components/pagination/pagination.css +5 -5
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +3 -2
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +1 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/select.js +9 -0
- package/src/components/sheet/sheet.css +1 -1
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +2 -2
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +1 -1
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +9 -5
- package/src/components/table/table.css +86 -11
- package/src/components/table/table.js +628 -57
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tooltip/tooltip.js +35 -17
- package/src/components/tree/tree.css +1 -1
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +12 -2
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/overlay-host.js +64 -9
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +5 -2
- package/src/internal/app-rail.js +134 -53
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/form.js +5 -3
- package/src/zeyos/index.js +3 -0
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +44 -13
- package/src/zeyos/table.js +220 -80
- package/src/zeyos/view.js +43 -93
- package/styles/base.css +16 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +56 -28
- package/styles/zx.css +3 -0
|
@@ -74,6 +74,11 @@ const FLOAT_STEP = 0.01;
|
|
|
74
74
|
* applies every committed edit.
|
|
75
75
|
* @property {FilterPanelSerialize} [serialize='seconds'] How a date leaves the component. A range's
|
|
76
76
|
* lower bound takes the start of its day and its upper bound the end, so a one-day range matches.
|
|
77
|
+
* @property {string|null} [timeZone=null] IANA zone the calendar days are counted in, such as the
|
|
78
|
+
* ZeyOS instance's `Europe/Vienna`. A day serializes as the first (or last) instant of that day
|
|
79
|
+
* in this zone, and an incoming instant shows as the day it falls on there — the same rule
|
|
80
|
+
* `zeyosForm()` and `zeyosTable()` apply to date-only fields. Null uses the runtime's own zone.
|
|
81
|
+
* An unknown zone throws a `RangeError`.
|
|
77
82
|
* @property {string|null} [emptyText=null] Text shown when `fields` is empty; null resolves
|
|
78
83
|
* `filterPanel.empty`.
|
|
79
84
|
* @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
|
|
@@ -111,6 +116,7 @@ export class FilterPanel extends Component {
|
|
|
111
116
|
value: {},
|
|
112
117
|
mode: 'draft',
|
|
113
118
|
serialize: 'seconds',
|
|
119
|
+
timeZone: null,
|
|
114
120
|
emptyText: null
|
|
115
121
|
};
|
|
116
122
|
|
|
@@ -130,9 +136,10 @@ export class FilterPanel extends Component {
|
|
|
130
136
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
131
137
|
this.el = root;
|
|
132
138
|
|
|
139
|
+
this._timeZone = normalizePanelTimeZone(this.options.timeZone);
|
|
133
140
|
this._fields = orderFields(this.options.fields, this.options.order);
|
|
134
|
-
this._draft = toDraft(this.options.value, this.options.fields);
|
|
135
|
-
this._applied = serializeFilterValue(this._draft, this.options.fields, this.options.serialize);
|
|
141
|
+
this._draft = toDraft(this.options.value, this.options.fields, this._timeZone);
|
|
142
|
+
this._applied = serializeFilterValue(this._draft, this.options.fields, this.options.serialize, this._timeZone);
|
|
136
143
|
this._appliedDraft = draftKey(this._draft);
|
|
137
144
|
|
|
138
145
|
const children = [];
|
|
@@ -187,7 +194,7 @@ export class FilterPanel extends Component {
|
|
|
187
194
|
* @fires FilterPanel#change
|
|
188
195
|
*/
|
|
189
196
|
setValue(value) {
|
|
190
|
-
this._draft = toDraft(value, this.options.fields);
|
|
197
|
+
this._draft = toDraft(value, this.options.fields, this._timeZone);
|
|
191
198
|
this._commit(this._draftValue());
|
|
192
199
|
this._writeControls();
|
|
193
200
|
this._syncActions();
|
|
@@ -219,7 +226,7 @@ export class FilterPanel extends Component {
|
|
|
219
226
|
* @fires FilterPanel#apply
|
|
220
227
|
*/
|
|
221
228
|
reset() {
|
|
222
|
-
return this._replaceDraft(toDraft(this.options.value, this.options.fields));
|
|
229
|
+
return this._replaceDraft(toDraft(this.options.value, this.options.fields, this._timeZone));
|
|
223
230
|
}
|
|
224
231
|
|
|
225
232
|
/**
|
|
@@ -233,7 +240,7 @@ export class FilterPanel extends Component {
|
|
|
233
240
|
* @fires FilterPanel#apply
|
|
234
241
|
*/
|
|
235
242
|
clear() {
|
|
236
|
-
return this._replaceDraft(toDraft({}, this.options.fields), { commit: true });
|
|
243
|
+
return this._replaceDraft(toDraft({}, this.options.fields, this._timeZone), { commit: true });
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
/** Destroys the composed controls and puts an enhanced target back as found. @returns {void} */
|
|
@@ -584,7 +591,7 @@ export class FilterPanel extends Component {
|
|
|
584
591
|
|
|
585
592
|
/** @returns {Record<string, unknown>} The draft, serialized and pruned. */
|
|
586
593
|
_draftValue() {
|
|
587
|
-
return serializeFilterValue(this._draft, this.options.fields, this.options.serialize);
|
|
594
|
+
return serializeFilterValue(this._draft, this.options.fields, this.options.serialize, this._timeZone);
|
|
588
595
|
}
|
|
589
596
|
|
|
590
597
|
/** Pushes the draft back into the controls without re-entering the edit path. @returns {void} */
|
|
@@ -703,42 +710,48 @@ export function pruneValue(value) {
|
|
|
703
710
|
|
|
704
711
|
/**
|
|
705
712
|
* Converts anything a calendar day can arrive as — a `YYYY-MM-DD` string, an ISO timestamp, a
|
|
706
|
-
* `Date`, Unix seconds or milliseconds — into the
|
|
713
|
+
* `Date`, Unix seconds or milliseconds — into the calendar day a native date input holds.
|
|
707
714
|
* Numbers below 1e11 are read as seconds, which is the boundary between the two epochs in use.
|
|
715
|
+
* An instant — Unix seconds or milliseconds, an ISO timestamp — names the day it falls on in
|
|
716
|
+
* `timeZone`. A `Date` is read as a picked day and keeps its local calendar day, matching
|
|
717
|
+
* `calendarDayToUnixSeconds()` in the ZeyOS binding.
|
|
708
718
|
* @param {unknown} value Candidate day.
|
|
719
|
+
* @param {string|null} [timeZone=null] IANA zone instants are read in; null for the runtime's own.
|
|
709
720
|
* @returns {string} `YYYY-MM-DD`, or an empty string when there is no day in it.
|
|
710
721
|
*/
|
|
711
|
-
export function toDateInput(value) {
|
|
722
|
+
export function toDateInput(value, timeZone = null) {
|
|
712
723
|
if (value === null || value === undefined || value === '') return '';
|
|
713
|
-
|
|
724
|
+
// A Date is a picked day — what Datebox and DatePicker hold — so its local day counts, as in
|
|
725
|
+
// `calendarDayToUnixSeconds()`; numbers and timestamps are instants, read in the zone.
|
|
726
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : zoneDay(value.getTime(), null);
|
|
714
727
|
if (typeof value === 'number') {
|
|
715
728
|
if (!Number.isFinite(value)) return '';
|
|
716
|
-
return
|
|
729
|
+
return zoneDay(Math.abs(value) < SECONDS_CUTOFF ? value * 1000 : value, timeZone);
|
|
717
730
|
}
|
|
718
731
|
const text = String(value).trim();
|
|
719
732
|
if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return text;
|
|
720
|
-
if (/^-?\d+(\.\d+)?$/.test(text)) return toDateInput(Number(text));
|
|
733
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return toDateInput(Number(text), timeZone);
|
|
721
734
|
const parsed = new Date(text);
|
|
722
|
-
return Number.isNaN(parsed.getTime()) ? '' :
|
|
735
|
+
return Number.isNaN(parsed.getTime()) ? '' : zoneDay(parsed.getTime(), timeZone);
|
|
723
736
|
}
|
|
724
737
|
|
|
725
738
|
/**
|
|
726
739
|
* Serializes one calendar day at one edge of a range. The `from` edge takes the first instant of
|
|
727
|
-
* its
|
|
728
|
-
* everything recorded on it.
|
|
740
|
+
* its day and the `to` edge the last, so a range whose ends name the same day still matches
|
|
741
|
+
* everything recorded on it. The day is counted in `timeZone`: with `Europe/Vienna`, 2026-03-10
|
|
742
|
+
* starts at 23:00 UTC on the 9th whatever zone the browser is in.
|
|
729
743
|
* @param {unknown} day Calendar day in any form `toDateInput()` accepts.
|
|
730
744
|
* @param {FilterPanelEdge} edge Which end of the range the day is.
|
|
731
745
|
* @param {FilterPanelSerialize} [serialize='seconds'] Output format, or a function receiving the
|
|
732
746
|
* edge instant as a `Date`.
|
|
747
|
+
* @param {string|null} [timeZone=null] IANA zone the day is counted in; null for the runtime's own.
|
|
733
748
|
* @returns {unknown} The serialized day, or null when there is no day.
|
|
734
749
|
*/
|
|
735
|
-
export function serializeDate(day, edge, serialize = 'seconds') {
|
|
736
|
-
const text = toDateInput(day);
|
|
750
|
+
export function serializeDate(day, edge, serialize = 'seconds', timeZone = null) {
|
|
751
|
+
const text = toDateInput(day, timeZone);
|
|
737
752
|
if (!text) return null;
|
|
738
753
|
const [year, month, date] = text.split('-').map(Number);
|
|
739
|
-
const instant = edge
|
|
740
|
-
? new Date(year, month - 1, date, 23, 59, 59, 999)
|
|
741
|
-
: new Date(year, month - 1, date, 0, 0, 0, 0);
|
|
754
|
+
const instant = new Date(dayEdge(year, month - 1, date, edge, timeZone));
|
|
742
755
|
if (typeof serialize === 'function') return serialize(instant, edge);
|
|
743
756
|
if (serialize === 'ms') return instant.getTime();
|
|
744
757
|
if (serialize === 'iso') return instant.toISOString();
|
|
@@ -775,9 +788,10 @@ export function normalizeOptions(options) {
|
|
|
775
788
|
* Running it on a draft returns the same draft, so it is safe on either shape.
|
|
776
789
|
* @param {Record<string, unknown>|null|undefined} value Incoming values keyed by field.
|
|
777
790
|
* @param {Record<string, FilterPanelField>} fields Field metadata.
|
|
791
|
+
* @param {string|null} [timeZone=null] IANA zone incoming instants are read in.
|
|
778
792
|
* @returns {Record<string, unknown>}
|
|
779
793
|
*/
|
|
780
|
-
export function toDraft(value, fields) {
|
|
794
|
+
export function toDraft(value, fields, timeZone = null) {
|
|
781
795
|
const source = isPlainObject(value) ? value : {};
|
|
782
796
|
/** @type {Record<string, unknown>} */
|
|
783
797
|
const draft = {};
|
|
@@ -792,8 +806,8 @@ export function toDraft(value, fields) {
|
|
|
792
806
|
draft[key] = selectionOf(raw, normalizeOptions(field.options));
|
|
793
807
|
} else if (spec.kind === 'date') {
|
|
794
808
|
draft[key] = spec.range
|
|
795
|
-
? { from: toDateInput(readEdge(raw, 'from')), to: toDateInput(readEdge(raw, 'to')) }
|
|
796
|
-
: toDateInput(raw);
|
|
809
|
+
? { from: toDateInput(readEdge(raw, 'from'), timeZone), to: toDateInput(readEdge(raw, 'to'), timeZone) }
|
|
810
|
+
: toDateInput(raw, timeZone);
|
|
797
811
|
} else {
|
|
798
812
|
draft[key] = spec.range
|
|
799
813
|
? { from: toNumberValue(readEdge(raw, 'from'), spec), to: toNumberValue(readEdge(raw, 'to'), spec) }
|
|
@@ -809,10 +823,11 @@ export function toDraft(value, fields) {
|
|
|
809
823
|
* @param {Record<string, unknown>|null|undefined} value Values keyed by field, raw or drafted.
|
|
810
824
|
* @param {Record<string, FilterPanelField>} fields Field metadata.
|
|
811
825
|
* @param {FilterPanelSerialize} [serialize='seconds'] Date output format.
|
|
826
|
+
* @param {string|null} [timeZone=null] IANA zone calendar days are counted in.
|
|
812
827
|
* @returns {Record<string, unknown>}
|
|
813
828
|
*/
|
|
814
|
-
export function serializeFilterValue(value, fields, serialize = 'seconds') {
|
|
815
|
-
const draft = toDraft(value, fields);
|
|
829
|
+
export function serializeFilterValue(value, fields, serialize = 'seconds', timeZone = null) {
|
|
830
|
+
const draft = toDraft(value, fields, timeZone);
|
|
816
831
|
/** @type {Record<string, unknown>} */
|
|
817
832
|
const result = {};
|
|
818
833
|
for (const [key, declared] of Object.entries(fields ?? {})) {
|
|
@@ -832,14 +847,14 @@ export function serializeFilterValue(value, fields, serialize = 'seconds') {
|
|
|
832
847
|
if (spec.kind === 'date') {
|
|
833
848
|
if (!spec.range) {
|
|
834
849
|
// A single date names a day, so it serializes as that day's first instant.
|
|
835
|
-
if (raw) result[key] = serializeDate(raw, 'from', serialize);
|
|
850
|
+
if (raw) result[key] = serializeDate(raw, 'from', serialize, timeZone);
|
|
836
851
|
continue;
|
|
837
852
|
}
|
|
838
853
|
const days = /** @type {{from: string, to: string}} */ (raw);
|
|
839
854
|
/** @type {Record<string, unknown>} */
|
|
840
855
|
const range = {};
|
|
841
|
-
if (days.from) range.from = serializeDate(days.from, 'from', serialize);
|
|
842
|
-
if (days.to) range.to = serializeDate(days.to, 'to', serialize);
|
|
856
|
+
if (days.from) range.from = serializeDate(days.from, 'from', serialize, timeZone);
|
|
857
|
+
if (days.to) range.to = serializeDate(days.to, 'to', serialize, timeZone);
|
|
843
858
|
if (Object.keys(range).length > 0) result[key] = range;
|
|
844
859
|
continue;
|
|
845
860
|
}
|
|
@@ -917,11 +932,113 @@ function toNumberValue(raw, spec) {
|
|
|
917
932
|
return spec.integer ? Math.trunc(parsed) : parsed;
|
|
918
933
|
}
|
|
919
934
|
|
|
920
|
-
/**
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
935
|
+
/**
|
|
936
|
+
* Validates the `timeZone` option.
|
|
937
|
+
* @param {unknown} timeZone IANA zone name; nullish or empty means the runtime's own zone.
|
|
938
|
+
* @returns {string|null} Canonical zone name, or null for the runtime's own zone.
|
|
939
|
+
* @throws {RangeError} When the runtime does not know the zone.
|
|
940
|
+
*/
|
|
941
|
+
export function normalizePanelTimeZone(timeZone) {
|
|
942
|
+
if (timeZone === null || timeZone === undefined || timeZone === '') return null;
|
|
943
|
+
try {
|
|
944
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: String(timeZone) }).resolvedOptions().timeZone;
|
|
945
|
+
} catch {
|
|
946
|
+
throw new RangeError(`Unknown time zone "${String(timeZone)}"`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* The calendar day an instant falls on, as `YYYY-MM-DD`, in a zone or the runtime's own.
|
|
952
|
+
* @param {number} milliseconds Epoch milliseconds.
|
|
953
|
+
* @param {string|null} timeZone IANA zone, or null.
|
|
954
|
+
* @returns {string}
|
|
955
|
+
*/
|
|
956
|
+
function zoneDay(milliseconds, timeZone) {
|
|
957
|
+
if (!timeZone) {
|
|
958
|
+
const date = new Date(milliseconds);
|
|
959
|
+
return isoDay(date.getFullYear(), date.getMonth(), date.getDate());
|
|
960
|
+
}
|
|
961
|
+
const wall = wallClock(milliseconds, timeZone);
|
|
962
|
+
return isoDay(wall.year, wall.month, wall.day);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Epoch milliseconds of the first (`from`) or last (`to`) instant of a calendar day in a zone.
|
|
967
|
+
* The last instant is one millisecond before the next day starts, which keeps a day that a
|
|
968
|
+
* daylight-saving change shortens or lengthens whole.
|
|
969
|
+
* @param {number} year Full year.
|
|
970
|
+
* @param {number} month Zero-based month.
|
|
971
|
+
* @param {number} day Day of the month.
|
|
972
|
+
* @param {FilterPanelEdge} edge Edge of the day.
|
|
973
|
+
* @param {string|null} timeZone IANA zone, or null for the runtime's own.
|
|
974
|
+
* @returns {number}
|
|
975
|
+
*/
|
|
976
|
+
function dayEdge(year, month, day, edge, timeZone) {
|
|
977
|
+
if (!timeZone) {
|
|
978
|
+
return (edge === 'to'
|
|
979
|
+
? new Date(year, month, day, 23, 59, 59, 999)
|
|
980
|
+
: new Date(year, month, day, 0, 0, 0, 0)).getTime();
|
|
981
|
+
}
|
|
982
|
+
if (edge !== 'to') return zoneMidnight(year, month, day, timeZone);
|
|
983
|
+
const next = new Date(Date.UTC(year, month, day + 1));
|
|
984
|
+
return zoneMidnight(next.getUTCFullYear(), next.getUTCMonth(), next.getUTCDate(), timeZone) - 1;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* The first instant of a calendar day in a zone: its midnight, or the first minute after a
|
|
989
|
+
* daylight-saving gap that swallows midnight. Two offset corrections settle it on either side of
|
|
990
|
+
* a change. This is the rule `zeyos/query.js` `calendarZone().midnightOf()` follows, so a day the
|
|
991
|
+
* panel serializes is the day a ZeyOS form stores.
|
|
992
|
+
* @param {number} year Full year.
|
|
993
|
+
* @param {number} month Zero-based month.
|
|
994
|
+
* @param {number} day Day of the month.
|
|
995
|
+
* @param {string} timeZone IANA zone.
|
|
996
|
+
* @returns {number} Epoch milliseconds.
|
|
997
|
+
*/
|
|
998
|
+
function zoneMidnight(year, month, day, timeZone) {
|
|
999
|
+
const target = Date.UTC(year, month, day);
|
|
1000
|
+
const offsetAt = (milliseconds) => {
|
|
1001
|
+
const wall = wallClock(milliseconds, timeZone);
|
|
1002
|
+
return Date.UTC(wall.year, wall.month, wall.day) + wall.seconds * 1000
|
|
1003
|
+
- Math.floor(milliseconds / 1000) * 1000;
|
|
1004
|
+
};
|
|
1005
|
+
const first = target - offsetAt(target);
|
|
1006
|
+
const second = target - offsetAt(first);
|
|
1007
|
+
let best = null;
|
|
1008
|
+
for (const candidate of [second, first]) {
|
|
1009
|
+
const wall = wallClock(candidate, timeZone);
|
|
1010
|
+
if (wall.year !== year || wall.month !== month || wall.day !== day) continue;
|
|
1011
|
+
if (!best || wall.seconds < best.seconds) best = { candidate, seconds: wall.seconds };
|
|
1012
|
+
}
|
|
1013
|
+
return best ? best.candidate : second;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* The wall clock of an instant in a zone.
|
|
1018
|
+
* @param {number} milliseconds Epoch milliseconds.
|
|
1019
|
+
* @param {string} timeZone IANA zone.
|
|
1020
|
+
* @returns {{year: number, month: number, day: number, seconds: number}} Zero-based month and
|
|
1021
|
+
* seconds since the zone's midnight.
|
|
1022
|
+
*/
|
|
1023
|
+
function wallClock(milliseconds, timeZone) {
|
|
1024
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
1025
|
+
timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric',
|
|
1026
|
+
hour: 'numeric', minute: 'numeric', second: 'numeric'
|
|
1027
|
+
});
|
|
1028
|
+
/** @type {Record<string, number>} */
|
|
1029
|
+
const parts = {};
|
|
1030
|
+
for (const part of formatter.formatToParts(new Date(milliseconds))) parts[part.type] = Number(part.value);
|
|
1031
|
+
return {
|
|
1032
|
+
year: parts.year,
|
|
1033
|
+
month: parts.month - 1,
|
|
1034
|
+
day: parts.day,
|
|
1035
|
+
seconds: (parts.hour % 24) * 3600 + parts.minute * 60 + parts.second
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** @param {number} year @param {number} month Zero-based. @param {number} day @returns {string} `YYYY-MM-DD`. */
|
|
1040
|
+
function isoDay(year, month, day) {
|
|
1041
|
+
return `${String(year).padStart(4, '0')}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
925
1042
|
}
|
|
926
1043
|
|
|
927
1044
|
/**
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
.zx-kanban-view__collapse:focus-visible,
|
|
135
135
|
.zx-kanban-view__move-handle:focus-visible,
|
|
136
136
|
.zx-kanban-view .zx-record-card:focus-visible {
|
|
137
|
-
outline:
|
|
137
|
+
outline-color: transparent;
|
|
138
138
|
box-shadow: var(--zx-focus-ring);
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
.zx-kanban-view :where(.zx-record-card[data-variant="outlined"]) {
|
|
191
|
-
box-shadow: var(--zx-shadow
|
|
191
|
+
box-shadow: var(--zx-surface-shadow);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/* The shared card anatomy stays intact; Kanban constrains media to a compact thumbnail. */
|
|
@@ -563,7 +563,7 @@
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
.zx-kanban-view__add:focus-visible {
|
|
566
|
-
outline:
|
|
566
|
+
outline-color: transparent;
|
|
567
567
|
box-shadow: var(--zx-focus-ring);
|
|
568
568
|
}
|
|
569
569
|
|
|
@@ -434,10 +434,11 @@ export class KanbanView extends RecordView {
|
|
|
434
434
|
|
|
435
435
|
this._toolbar = h('div', { class: 'zx-record-view__toolbar zx-kanban-view__toolbar' });
|
|
436
436
|
// A label the host passed in the object form wins; otherwise Kanban's own
|
|
437
|
-
// message bag supplies it, which is where this label has always come from.
|
|
437
|
+
// message bag supplies it, which is where this label has always come from. Mounted through
|
|
438
|
+
// the shared path so `fieldControls.target` places it in a host element, as in every view.
|
|
438
439
|
if (options.fieldControls) {
|
|
439
440
|
const configured = normalizeViewFieldControls(options.fieldControls).label;
|
|
440
|
-
this.
|
|
441
|
+
this._mountViewFieldControls(this._toolbar, configured ?? this._text('fields'));
|
|
441
442
|
}
|
|
442
443
|
if (options.searchControl) this._toolbar.append(this._createSearchControl());
|
|
443
444
|
if (options.historyControls) this._toolbar.append(this._createHistoryControls());
|
|
@@ -468,6 +469,8 @@ export class KanbanView extends RecordView {
|
|
|
468
469
|
if (options.contextMenu) this._createContextMenu();
|
|
469
470
|
return root;
|
|
470
471
|
} catch (error) {
|
|
472
|
+
// An external `fieldControls.target` must not keep a chooser whose view never came to be.
|
|
473
|
+
this._removeViewFieldControls();
|
|
471
474
|
if (this._kanbanSnapshot) restoreTarget(root, this._kanbanSnapshot);
|
|
472
475
|
throw error;
|
|
473
476
|
}
|
|
@@ -2,7 +2,8 @@ import { Component } from '../../core/component.js';
|
|
|
2
2
|
import { h, safeHref } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
4
|
import { printf } from '../../core/i18n.js';
|
|
5
|
-
import { connectedOverlayHost,
|
|
5
|
+
import { connectedOverlayHost, overlayScope } from '../../core/overlay-host.js';
|
|
6
|
+
import { pushOverlay } from '../../core/overlay-stack.js';
|
|
6
7
|
import { uid } from '../../core/util.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -158,6 +159,8 @@ export class Launcher extends Component {
|
|
|
158
159
|
throw new TypeError('Launcher target must be a <dialog> element');
|
|
159
160
|
}
|
|
160
161
|
this._createdRoot = this.el === null;
|
|
162
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} Entry on the overlay stack while open. */
|
|
163
|
+
this._overlay = null;
|
|
161
164
|
const dialog = /** @type {HTMLDialogElement} */ (this.el ?? h('dialog'));
|
|
162
165
|
this.el = dialog;
|
|
163
166
|
this._original = this._createdRoot ? null : {
|
|
@@ -266,7 +269,9 @@ export class Launcher extends Component {
|
|
|
266
269
|
dialog.dataset.state = 'closed';
|
|
267
270
|
// A scope that is not in the document yet cannot say which themed region owns the dialog; it
|
|
268
271
|
// stays detached until open() hosts it.
|
|
269
|
-
|
|
272
|
+
// The theme scope, never an open dialog: a modal opened from inside another stacks above it in
|
|
273
|
+
// the top layer wherever it lives, and nested it would vanish if the outer one closed first.
|
|
274
|
+
if (this._createdRoot) connectedOverlayHost(this.options.scope, { dialog: false })?.append(dialog);
|
|
270
275
|
|
|
271
276
|
this.listen(input, 'input', () => {
|
|
272
277
|
this._query = /** @type {HTMLInputElement} */ (input).value;
|
|
@@ -299,6 +304,7 @@ export class Launcher extends Component {
|
|
|
299
304
|
this.close();
|
|
300
305
|
});
|
|
301
306
|
this.listen(dialog, 'close', () => {
|
|
307
|
+
if (!dialog.open) this._releaseOverlay();
|
|
302
308
|
dialog.dataset.state = 'closed';
|
|
303
309
|
input.setAttribute('aria-expanded', 'false');
|
|
304
310
|
this._abortRequest();
|
|
@@ -336,11 +342,18 @@ export class Launcher extends Component {
|
|
|
336
342
|
return this;
|
|
337
343
|
}
|
|
338
344
|
if (this._createdRoot) {
|
|
339
|
-
const host =
|
|
345
|
+
const host = overlayScope(this.options.scope);
|
|
340
346
|
if (this.el.parentElement !== host) host.append(this.el);
|
|
341
347
|
}
|
|
342
348
|
const before = this._pages.map((entry) => entry.page);
|
|
349
|
+
const opener = document.activeElement;
|
|
343
350
|
this.el.showModal();
|
|
351
|
+
/*
|
|
352
|
+
* A modal on the overlay stack, like every Modal: Escape and presses inside it then reach no
|
|
353
|
+
* popup left open behind it, and toast regions move into it while it is open.
|
|
354
|
+
*/
|
|
355
|
+
this._releaseOverlay();
|
|
356
|
+
this._overlay = pushOverlay({ kind: 'modal', root: this.el, owner: this, anchor: opener });
|
|
344
357
|
this.el.dataset.state = 'open';
|
|
345
358
|
this.refs.input.setAttribute('aria-expanded', 'true');
|
|
346
359
|
this._goTo(target == null ? [] : this._pagePath(target), { emit: false });
|
|
@@ -443,6 +456,17 @@ export class Launcher extends Component {
|
|
|
443
456
|
return this;
|
|
444
457
|
}
|
|
445
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Takes the launcher off the overlay stack. Safe to call repeatedly.
|
|
461
|
+
* @returns {void}
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
_releaseOverlay() {
|
|
465
|
+
const overlay = this._overlay;
|
|
466
|
+
this._overlay = null;
|
|
467
|
+
overlay?.release();
|
|
468
|
+
}
|
|
469
|
+
|
|
446
470
|
/** Aborts sources and removes the owned dialog. @returns {void} */
|
|
447
471
|
destroy() {
|
|
448
472
|
if (this._destroyed) return;
|
|
@@ -450,6 +474,8 @@ export class Launcher extends Component {
|
|
|
450
474
|
const original = this._original;
|
|
451
475
|
this._abortRequest();
|
|
452
476
|
if (this.isOpen()) this.el.close();
|
|
477
|
+
// The queued close that would release it never arrives once listeners are aborted.
|
|
478
|
+
this._releaseOverlay();
|
|
453
479
|
super.destroy();
|
|
454
480
|
if (!this._createdRoot && original) {
|
|
455
481
|
for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
|
|
@@ -7,8 +7,10 @@ import { badge } from '../badge/badge.js';
|
|
|
7
7
|
import { Search } from '../search/search.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Built-in view shapes, so `views: ['
|
|
11
|
-
* that wants other words hands descriptors instead.
|
|
10
|
+
* Built-in view shapes, so `views: ['table', 'list', 'cards', 'gallery', 'board']` needs no labels
|
|
11
|
+
* of its own. A host that wants other words hands descriptors instead. Each shape has the glyph
|
|
12
|
+
* the overview tools people already use give it: a ruled grid for the table, lines for the list,
|
|
13
|
+
* tiles for cards, a picture for the gallery, and columns for the board.
|
|
12
14
|
*
|
|
13
15
|
* `auto` deliberately carries no icon: no glyph in the set says "whatever fits", and an icon that
|
|
14
16
|
* has to be explained is worse than the word.
|
|
@@ -16,9 +18,11 @@ import { Search } from '../search/search.js';
|
|
|
16
18
|
*/
|
|
17
19
|
const VIEW_PRESETS = Object.freeze({
|
|
18
20
|
auto: { icon: null, key: 'listToolbar.viewAuto', fallback: 'Auto' },
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
table: { icon: 'table', key: 'listToolbar.viewTable', fallback: 'Table' },
|
|
22
|
+
list: { icon: 'list', key: 'listToolbar.viewList', fallback: 'List' },
|
|
23
|
+
cards: { icon: 'grid', key: 'listToolbar.viewCards', fallback: 'Cards' },
|
|
24
|
+
gallery: { icon: 'image', key: 'listToolbar.viewGallery', fallback: 'Gallery' },
|
|
25
|
+
board: { icon: 'columns', key: 'listToolbar.viewBoard', fallback: 'Board' }
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -378,8 +382,10 @@ export class ListToolbar extends Component {
|
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
/**
|
|
381
|
-
* Optional sugar for a `TableView` or `
|
|
382
|
-
* mounts the view's field chooser into this bar
|
|
385
|
+
* Optional sugar for a `TableView`, `CardView`, or `KanbanView`: it wires the shape switch to
|
|
386
|
+
* the view and mounts the view's field chooser into this bar — through the view's public
|
|
387
|
+
* `mountFieldControls()` when it has one, which every `RecordView` does. A `TableView` with the
|
|
388
|
+
* default `fieldControls: true` contributes its Table's column chooser.
|
|
383
389
|
*
|
|
384
390
|
* Nothing is imported and nothing is required — every capability is feature-detected, so a view
|
|
385
391
|
* that offers none of them still works and still gets `viewchange`, and the host swaps the view
|
|
@@ -409,10 +415,19 @@ export class ListToolbar extends Component {
|
|
|
409
415
|
* @returns {this}
|
|
410
416
|
*/
|
|
411
417
|
unbind() {
|
|
418
|
+
const bound = this._bound;
|
|
412
419
|
this._bound = null;
|
|
413
420
|
if (this._chooser) {
|
|
414
|
-
|
|
421
|
+
const chooser = this._chooser;
|
|
415
422
|
this._chooser = null;
|
|
423
|
+
const view = bound?.view;
|
|
424
|
+
// The view put it here and knows where it came from; hand it back only while it is still
|
|
425
|
+
// ours, since a destroyed view has already taken it away.
|
|
426
|
+
if (chooser.viaView && view && typeof view.mountFieldControls === 'function') {
|
|
427
|
+
if (this.el.contains(chooser.element)) view.mountFieldControls(null);
|
|
428
|
+
} else {
|
|
429
|
+
restorePosition(chooser, this.el);
|
|
430
|
+
}
|
|
416
431
|
}
|
|
417
432
|
return this;
|
|
418
433
|
}
|
|
@@ -597,21 +612,28 @@ export class ListToolbar extends Component {
|
|
|
597
612
|
if (view.options?.fieldControls === false) return;
|
|
598
613
|
const element = this._findFieldControls(view);
|
|
599
614
|
if (!element || this.el.contains(element)) return;
|
|
615
|
+
if (typeof view.mountFieldControls === 'function') {
|
|
616
|
+
const mounted = view.mountFieldControls(this.refs.tools);
|
|
617
|
+
if (isElement(mounted) && this.el.contains(mounted)) {
|
|
618
|
+
this._chooser = { element: mounted, parent: null, next: null, viaView: true };
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
600
622
|
this._chooser = capturePosition(element);
|
|
601
623
|
this.refs.tools.append(element);
|
|
602
624
|
}
|
|
603
625
|
|
|
604
626
|
/**
|
|
605
|
-
* Finds a view's field chooser without importing the view
|
|
627
|
+
* Finds a view's field chooser without importing the view: its public `getFieldControls()`,
|
|
628
|
+
* else — for an object that only looks like a record view — the shared chooser's class.
|
|
606
629
|
* @param {Record<string, any>} view Bound view.
|
|
607
630
|
* @returns {Element|null}
|
|
608
631
|
*/
|
|
609
632
|
_findFieldControls(view) {
|
|
610
633
|
if (typeof view.getFieldControls === 'function') {
|
|
611
634
|
const element = view.getFieldControls();
|
|
612
|
-
|
|
635
|
+
return isElement(element) ? element : null;
|
|
613
636
|
}
|
|
614
|
-
if (isElement(view._viewFieldControls)) return view._viewFieldControls;
|
|
615
637
|
const root = typeof view.toElement === 'function' ? view.toElement() : null;
|
|
616
638
|
return isElement(root) ? root.querySelector('.zx-record-view__field-controls') : null;
|
|
617
639
|
}
|
|
@@ -661,7 +683,10 @@ export class ListToolbar extends Component {
|
|
|
661
683
|
* @property {((event: MouseEvent) => void)|null} onclick Descriptor callback.
|
|
662
684
|
*/
|
|
663
685
|
|
|
664
|
-
/**
|
|
686
|
+
/**
|
|
687
|
+
* @typedef {{element: Element, parent: Node|null, next: Node|null, viaView?: boolean}} AdoptedElement
|
|
688
|
+
* `viaView` marks a field chooser the bound view moved here itself and takes back on `unbind()`.
|
|
689
|
+
*/
|
|
665
690
|
|
|
666
691
|
/**
|
|
667
692
|
* @typedef {Object} RecordViewCapabilities
|
|
@@ -669,7 +694,8 @@ export class ListToolbar extends Component {
|
|
|
669
694
|
* @property {string|null} setShape Method that changes the view shape, if any.
|
|
670
695
|
* @property {string|null} getShape Method that reads the view shape, if any.
|
|
671
696
|
* @property {boolean} events Whether the object emits component events.
|
|
672
|
-
* @property {boolean} fieldControls Whether the object offers its field chooser through a method
|
|
697
|
+
* @property {boolean} fieldControls Whether the object offers its field chooser through a method
|
|
698
|
+
* (`getFieldControls()`, which every `RecordView` has).
|
|
673
699
|
*/
|
|
674
700
|
|
|
675
701
|
/**
|
|
@@ -89,7 +89,6 @@ export class MasterPanel extends Component {
|
|
|
89
89
|
|
|
90
90
|
const moduleName = normalizeModule(this.options.module);
|
|
91
91
|
root.dataset.module = moduleName;
|
|
92
|
-
root.style.setProperty('--zx-master-panel-accent', `var(--zx-module-${moduleName})`);
|
|
93
92
|
this.setTitle(this.options.title);
|
|
94
93
|
if (this.options.content !== null) this.setContent(this.options.content);
|
|
95
94
|
else content.append(...originalContent);
|
|
@@ -129,3 +129,66 @@
|
|
|
129
129
|
margin: var(--zx-space-1) var(--zx-space-2);
|
|
130
130
|
background: var(--zx-color-overlay-divider);
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* A choice: a setting chosen in place — its label, then a small segmented control of options, the
|
|
135
|
+
* checked one lifted out of the track. Neutral, not the accent: it is a state, not an action.
|
|
136
|
+
*/
|
|
137
|
+
.zx-menu-button__choice {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
gap: var(--zx-space-3);
|
|
142
|
+
min-block-size: var(--zx-control-height);
|
|
143
|
+
padding: var(--zx-space-1) var(--zx-space-2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.zx-menu-button__choice-label {
|
|
147
|
+
color: var(--zx-color-text);
|
|
148
|
+
font-size: var(--zx-text-md);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.zx-menu-button__choice-options {
|
|
152
|
+
display: inline-flex;
|
|
153
|
+
flex: none;
|
|
154
|
+
gap: 2px;
|
|
155
|
+
border-radius: var(--zx-radius-md);
|
|
156
|
+
background: var(--zx-color-bg-hover);
|
|
157
|
+
padding: 2px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.zx-menu-button__choice-option {
|
|
161
|
+
display: grid;
|
|
162
|
+
place-items: center;
|
|
163
|
+
inline-size: var(--zx-space-6);
|
|
164
|
+
block-size: var(--zx-space-6);
|
|
165
|
+
border: 0;
|
|
166
|
+
border-radius: var(--zx-radius-sm);
|
|
167
|
+
background: transparent;
|
|
168
|
+
padding: 0;
|
|
169
|
+
color: var(--zx-color-text-muted);
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.zx-menu-button__choice-option:hover { color: var(--zx-color-text); }
|
|
174
|
+
|
|
175
|
+
.zx-menu-button__choice-option[aria-checked="true"] {
|
|
176
|
+
background: var(--zx-color-bg-surface);
|
|
177
|
+
box-shadow: 0 0 0 1px var(--zx-color-border-subtle);
|
|
178
|
+
color: var(--zx-color-text);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.zx-menu-button__choice-option[aria-disabled="true"] { cursor: not-allowed; opacity: .55; }
|
|
182
|
+
|
|
183
|
+
.zx-menu-button__choice-option:focus-visible {
|
|
184
|
+
outline-color: transparent;
|
|
185
|
+
box-shadow: var(--zx-focus-ring);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (forced-colors: active) {
|
|
189
|
+
.zx-menu-button__choice-option[aria-checked="true"] {
|
|
190
|
+
background: Highlight;
|
|
191
|
+
color: HighlightText;
|
|
192
|
+
forced-color-adjust: none;
|
|
193
|
+
}
|
|
194
|
+
}
|