@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { printf } from '../../core/i18n.js';
|
|
5
|
+
import { Select } from '../select/select.js';
|
|
4
6
|
import {
|
|
5
7
|
cloneFilterAst, cloneFilterValue, emptyFilterAst, filterCondition, filterGroup, filterOperators,
|
|
6
8
|
parseFilterAst, validateFilterAst
|
|
@@ -28,11 +30,16 @@ import {
|
|
|
28
30
|
* @property {boolean} [allowGroups=true] Whether nested groups may be authored.
|
|
29
31
|
* @property {number} [maxDepth=3] Maximum nested group depth.
|
|
30
32
|
* @property {number} [maxConditions=50] Maximum condition count.
|
|
33
|
+
* @property {boolean} [searchable=false] Searchable field, operator and single-choice controls.
|
|
34
|
+
* @property {'standard'|'compact'} [layout='standard'] Condition layout; compact rows adapt to container width.
|
|
35
|
+
* @property {'and'|'or'|null} [rootLogic=null] Restrict root logic without rewriting incompatible saved expressions.
|
|
36
|
+
* @property {boolean} [showApply=true] Show the internal Apply button.
|
|
37
|
+
* @property {boolean} [showRootActions=true] Show Add controls inside the root group as well as the footer.
|
|
31
38
|
* @property {boolean} [autoApply=false] Emit apply after every valid mutation.
|
|
32
39
|
* @property {boolean} [readonly=false] Prevent mutations while keeping values readable.
|
|
33
40
|
* @property {boolean} [disabled=false] Disable all controls.
|
|
34
|
-
* @property {string} [applyLabel
|
|
35
|
-
* @property {string} [clearLabel
|
|
41
|
+
* @property {string} [applyLabel] Apply button label. Omitted resolves `filter.apply`, English `Apply filters`.
|
|
42
|
+
* @property {string} [clearLabel] Clear button label. Omitted resolves `filter.clear`, English `Clear`.
|
|
36
43
|
* @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onchange] Draft-change listener.
|
|
37
44
|
* @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onapply] Valid-apply listener.
|
|
38
45
|
* @property {(event:CustomEvent<Record<string,unknown>>)=>void} [oninvalid] Invalid-apply listener.
|
|
@@ -41,6 +48,29 @@ import {
|
|
|
41
48
|
* @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onerror] Async failure listener.
|
|
42
49
|
*/
|
|
43
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Words the value builders render, resolved by the component that owns them.
|
|
53
|
+
* @typedef {Object} FilterValueMessages
|
|
54
|
+
* @property {string} choose Empty-choice option label.
|
|
55
|
+
* @property {string} yes Boolean true label.
|
|
56
|
+
* @property {string} no Boolean false label.
|
|
57
|
+
* @property {string} commaSeparated Placeholder for a many-valued text input.
|
|
58
|
+
* @property {(label: string) => string} value Accessible name for a single value control.
|
|
59
|
+
* @property {(label: string, position: number) => string} valueAt Accessible name for one end of a pair.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** English text the value builders fall back to when they are called without a component. */
|
|
63
|
+
const BUILT_IN_VALUE_MESSAGES = Object.freeze({
|
|
64
|
+
choose: 'Choose…',
|
|
65
|
+
yes: 'True',
|
|
66
|
+
no: 'False',
|
|
67
|
+
commaSeparated: 'Comma-separated values',
|
|
68
|
+
/** @param {string} label @returns {string} */
|
|
69
|
+
value: (label) => `${label} value`,
|
|
70
|
+
/** @param {string} label @param {number} position @returns {string} */
|
|
71
|
+
valueAt: (label, position) => `${label} value ${position}`
|
|
72
|
+
});
|
|
73
|
+
|
|
44
74
|
/**
|
|
45
75
|
* Backend-neutral dynamic filter expression editor.
|
|
46
76
|
* @fires Filter#change
|
|
@@ -58,7 +88,8 @@ export class Filter extends Component {
|
|
|
58
88
|
static defaults = {
|
|
59
89
|
fields: [], value: null, operators: [], allowGroups: true, maxDepth: 3, maxConditions: 50,
|
|
60
90
|
autoApply: false, readonly: false, disabled: false,
|
|
61
|
-
|
|
91
|
+
searchable: false, layout: 'standard', rootLogic: null, showApply: true, showRootActions: true,
|
|
92
|
+
applyLabel: null, clearLabel: null
|
|
62
93
|
};
|
|
63
94
|
|
|
64
95
|
/** @returns {HTMLElement} */
|
|
@@ -73,6 +104,9 @@ export class Filter extends Component {
|
|
|
73
104
|
this._disabled = Boolean(this.options.disabled);
|
|
74
105
|
this._async = new Map();
|
|
75
106
|
this._customEditors = [];
|
|
107
|
+
this._selectEditors = [];
|
|
108
|
+
this._disposed = false;
|
|
109
|
+
root.dataset.layout = this.options.layout === 'compact' ? 'compact' : 'standard';
|
|
76
110
|
this._value = this.options.value == null ? emptyFilterAst()
|
|
77
111
|
: parseFilterAst(this.options.value, this._limits());
|
|
78
112
|
|
|
@@ -157,8 +191,10 @@ export class Filter extends Component {
|
|
|
157
191
|
if (!removed) return this;
|
|
158
192
|
this._abortTree(removed);
|
|
159
193
|
this._render();
|
|
160
|
-
const label = removed.kind === 'condition'
|
|
161
|
-
|
|
194
|
+
const label = removed.kind === 'condition'
|
|
195
|
+
? this._field(removed.field)?.label ?? this._message('filter.conditionNoun', 'Filter')
|
|
196
|
+
: this._message('filter.groupNoun', 'Filter group');
|
|
197
|
+
this._announce(this._message('filter.removed', '%1 removed', label));
|
|
162
198
|
this._changed('remove', id);
|
|
163
199
|
this.focus(focusId);
|
|
164
200
|
return this;
|
|
@@ -185,7 +221,12 @@ export class Filter extends Component {
|
|
|
185
221
|
}
|
|
186
222
|
|
|
187
223
|
/** Returns semantic validity and errors. @returns {{valid:boolean,errors:Array<Record<string,string>>}} */
|
|
188
|
-
validate() {
|
|
224
|
+
validate() {
|
|
225
|
+
const result = validateFilterAst(this._value, this._fields, this._operators);
|
|
226
|
+
const error = rootLogicError(this._value, this.options.rootLogic);
|
|
227
|
+
if (error) { result.valid = false; result.errors.push(error); }
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
189
230
|
|
|
190
231
|
/** Applies only a valid AST. @returns {FilterAst|null} */
|
|
191
232
|
apply() {
|
|
@@ -215,9 +256,13 @@ export class Filter extends Component {
|
|
|
215
256
|
/** Focuses a condition's field or the Add button. @param {string|null} [id=null] @returns {this} */
|
|
216
257
|
focus(id = null) {
|
|
217
258
|
const selector = id
|
|
218
|
-
? `[data-node-id="${cssEscape(id)}"] :is([data-filter-field], [data-filter-logic], [data-filter-action="add"])`
|
|
259
|
+
? `[data-node-id="${cssEscape(id)}"] :is([data-filter-field], [data-filter-focus="field"], [data-filter-logic], [data-filter-action="add"])`
|
|
219
260
|
: '[data-filter-action="add"]';
|
|
220
|
-
queueMicrotask(() =>
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
if (this._disposed) return;
|
|
263
|
+
const target = /** @type {HTMLElement|null} */ (this.el.querySelector(selector)) ?? this.refs.add;
|
|
264
|
+
target?.focus();
|
|
265
|
+
});
|
|
221
266
|
return this;
|
|
222
267
|
}
|
|
223
268
|
|
|
@@ -230,6 +275,8 @@ export class Filter extends Component {
|
|
|
230
275
|
|
|
231
276
|
/** @returns {void} */
|
|
232
277
|
destroy() {
|
|
278
|
+
this._disposed = true;
|
|
279
|
+
this._renderVersion += 1;
|
|
233
280
|
this._abortAll();
|
|
234
281
|
this._destroyEditors();
|
|
235
282
|
const root = this.el;
|
|
@@ -245,10 +292,10 @@ export class Filter extends Component {
|
|
|
245
292
|
const status = h('div', { ref: 'status', class: 'zx-filter__status', role: 'status', ariaLive: 'polite' });
|
|
246
293
|
const tree = this._renderGroup(this._value.root, 0, true);
|
|
247
294
|
const actions = h('div', { class: 'zx-filter__actions' },
|
|
248
|
-
actionButton('add', this._value.root.id, 'plus', 'Add filter', { ref: 'add', disabled: this._blocked() }),
|
|
249
|
-
this.options.allowGroups ? actionButton('add-group', this._value.root.id, 'folder', 'Add group', { disabled: this._blocked() }) : null,
|
|
250
|
-
actionButton('clear', '', 'x', String(this.options.clearLabel), { disabled: this._blocked() }),
|
|
251
|
-
actionButton('apply', '', 'check', String(this.options.applyLabel), {
|
|
295
|
+
actionButton('add', this._value.root.id, 'plus', this._message('filter.addFilter', 'Add filter'), { ref: 'add', disabled: this._blocked() }),
|
|
296
|
+
this.options.allowGroups ? actionButton('add-group', this._value.root.id, 'folder', this._message('filter.addGroup', 'Add group'), { disabled: this._blocked() }) : null,
|
|
297
|
+
actionButton('clear', '', 'x', String(this.options.clearLabel ?? this._message('filter.clear', 'Clear')), { disabled: this._blocked() }),
|
|
298
|
+
this.options.showApply ? actionButton('apply', '', 'check', String(this.options.applyLabel ?? this._message('filter.apply', 'Apply filters')), { kind: 'primary', disabled: this._disabled }) : null
|
|
252
299
|
);
|
|
253
300
|
this.el.replaceChildren(status, tree, actions);
|
|
254
301
|
this.refs.status = status;
|
|
@@ -266,20 +313,28 @@ export class Filter extends Component {
|
|
|
266
313
|
group.children.map((node) => node.kind === 'group' ? this._renderGroup(node, depth + 1, false) : this._renderCondition(node)));
|
|
267
314
|
const logic = h('select', {
|
|
268
315
|
class: 'zx-filter__logic', dataset: { filterLogic: '', nodeId: group.id },
|
|
269
|
-
ariaLabel: root ? '
|
|
316
|
+
ariaLabel: root ? this._message('filter.rootLogic', 'Root filter logic')
|
|
317
|
+
: this._message('filter.groupLogic', 'Group filter logic'),
|
|
318
|
+
disabled: this._blocked()
|
|
270
319
|
},
|
|
271
|
-
h('option', { value: 'and', selected: group.logic === 'and' }, 'Match all'),
|
|
272
|
-
h('option', { value: 'or', selected: group.logic === 'or' }, 'Match any'));
|
|
273
|
-
const legend = h('legend', { class: 'zx-filter__legend' },
|
|
320
|
+
h('option', { value: 'and', selected: group.logic === 'and' }, this._message('filter.matchAll', 'Match all')),
|
|
321
|
+
h('option', { value: 'or', selected: group.logic === 'or' }, this._message('filter.matchAny', 'Match any')));
|
|
322
|
+
const legend = h('legend', { class: 'zx-filter__legend' },
|
|
323
|
+
root && this.options.rootLogic
|
|
324
|
+
? (group.logic === 'or'
|
|
325
|
+
? this._message('filter.matchAnyConditions', 'Match any conditions')
|
|
326
|
+
: this._message('filter.matchAllConditions', 'Match all conditions'))
|
|
327
|
+
: [logic, h('span', {}, root ? this._message('filter.conditions', 'conditions')
|
|
328
|
+
: this._message('filter.inThisGroup', 'in this group'))]);
|
|
274
329
|
const controls = h('div', { class: 'zx-filter__group-actions' },
|
|
275
|
-
actionButton('add', group.id, 'plus', 'Add filter', { disabled: this._blocked() }),
|
|
330
|
+
actionButton('add', group.id, 'plus', this._message('filter.addFilter', 'Add filter'), { disabled: this._blocked() }),
|
|
276
331
|
this.options.allowGroups && depth < Number(this.options.maxDepth)
|
|
277
|
-
? actionButton('add-group', group.id, 'folder', 'Add subgroup', { disabled: this._blocked() }) : null,
|
|
278
|
-
root ? null : actionButton('remove', group.id, 'trash', 'Remove group', { disabled: this._blocked() })
|
|
332
|
+
? actionButton('add-group', group.id, 'folder', this._message('filter.addSubgroup', 'Add subgroup'), { disabled: this._blocked() }) : null,
|
|
333
|
+
root ? null : actionButton('remove', group.id, 'trash', this._message('filter.removeGroup', 'Remove group'), { disabled: this._blocked() })
|
|
279
334
|
);
|
|
280
335
|
return h(root ? 'fieldset' : 'li', {
|
|
281
336
|
class: 'zx-filter__group', dataset: { nodeId: group.id, depth: String(depth) }
|
|
282
|
-
}, root ? legend : h('fieldset', {}, legend, children, controls), root ? children : null, root ? controls : null);
|
|
337
|
+
}, root ? legend : h('fieldset', {}, legend, children, controls), root ? children : null, root && this.options.showRootActions ? controls : null);
|
|
283
338
|
}
|
|
284
339
|
|
|
285
340
|
/** @param {any} node @returns {HTMLElement} */
|
|
@@ -287,24 +342,35 @@ export class Filter extends Component {
|
|
|
287
342
|
const field = this._field(node.field);
|
|
288
343
|
const operators = this._operatorsFor(field);
|
|
289
344
|
const operator = this._operator(node.operator);
|
|
290
|
-
const
|
|
345
|
+
const fieldChoices = this._fields.map((candidate) => ({value: candidate.id, label: candidate.label}));
|
|
346
|
+
if (node.field && !field) fieldChoices.push({value: node.field, label: this._unavailable(node.field)});
|
|
347
|
+
const operatorChoices = operators.map((candidate) => ({value: candidate.id, label: candidate.label}));
|
|
348
|
+
if (node.operator && !operators.some((candidate) => candidate.id === node.operator)) operatorChoices.push({value: node.operator, label: this._unavailable(node.operator)});
|
|
349
|
+
const fieldSelect = this.options.searchable ? this._searchSelect(fieldChoices, node.field, this._message('filter.field', 'Filter field'), node.id, 'field', (value) => {
|
|
350
|
+
const selectedField = this._field(String(value));
|
|
351
|
+
this.update(node.id, {field: String(value), operator: this._defaultOperator(selectedField) ?? '', value: null});
|
|
352
|
+
this.focus(node.id);
|
|
353
|
+
}) : h('select', {
|
|
291
354
|
class: 'zx-filter__field', dataset: { filterField: '', nodeId: node.id },
|
|
292
|
-
ariaLabel: 'Filter field', disabled: this._blocked()
|
|
355
|
+
ariaLabel: this._message('filter.field', 'Filter field'), disabled: this._blocked()
|
|
293
356
|
},
|
|
294
|
-
h('option', { value: '', selected: !node.field }, 'Choose a field…'),
|
|
357
|
+
h('option', { value: '', selected: !node.field }, this._message('filter.chooseField', 'Choose a field…')),
|
|
295
358
|
this._fields.map((candidate) => h('option', { value: candidate.id, selected: candidate.id === node.field }, candidate.label)),
|
|
296
|
-
node.field && !field ? h('option', { value: node.field, selected: true },
|
|
297
|
-
const operatorSelect =
|
|
359
|
+
node.field && !field ? h('option', { value: node.field, selected: true }, this._unavailable(node.field)) : null);
|
|
360
|
+
const operatorSelect = this.options.searchable ? this._searchSelect(operatorChoices, node.operator, this._message('filter.operator', 'Filter operator'), node.id, 'operator', (value) => {
|
|
361
|
+
this.update(node.id, {operator: String(value), value: null});
|
|
362
|
+
queueMicrotask(() => this.el.querySelector(`[data-node-id="${cssEscape(node.id)}"] :is([data-filter-value], [data-filter-focus="value"])`)?.focus());
|
|
363
|
+
}) : h('select', {
|
|
298
364
|
class: 'zx-filter__operator', dataset: { filterOperator: '', nodeId: node.id },
|
|
299
|
-
ariaLabel: 'Filter operator', disabled: this._blocked()
|
|
365
|
+
ariaLabel: this._message('filter.operator', 'Filter operator'), disabled: this._blocked()
|
|
300
366
|
},
|
|
301
|
-
h('option', { value: '', selected: !node.operator }, 'Choose an operator…'),
|
|
367
|
+
h('option', { value: '', selected: !node.operator }, this._message('filter.chooseOperator', 'Choose an operator…')),
|
|
302
368
|
operators.map((candidate) => h('option', { value: candidate.id, selected: candidate.id === node.operator }, candidate.label)),
|
|
303
369
|
node.operator && !operators.some((candidate) => candidate.id === node.operator)
|
|
304
|
-
? h('option', { value: node.operator, selected: true },
|
|
370
|
+
? h('option', { value: node.operator, selected: true }, this._unavailable(node.operator)) : null);
|
|
305
371
|
return h('li', { class: 'zx-filter__condition', dataset: { nodeId: node.id } },
|
|
306
372
|
fieldSelect, operatorSelect, this._renderValue(node, field, operator),
|
|
307
|
-
|
|
373
|
+
this._removeConditionButton(node, field)
|
|
308
374
|
);
|
|
309
375
|
}
|
|
310
376
|
|
|
@@ -312,7 +378,7 @@ export class Filter extends Component {
|
|
|
312
378
|
_renderValue(node, field, operator) {
|
|
313
379
|
const host = h('div', { class: 'zx-filter__value' });
|
|
314
380
|
if (!field || !operator) return host;
|
|
315
|
-
if (operator.arity === 'none') return h('div', { class: 'zx-filter__value zx-filter__value--empty' }, 'No value');
|
|
381
|
+
if (operator.arity === 'none') return h('div', { class: 'zx-filter__value zx-filter__value--empty' }, this._message('filter.noValue', 'No value'));
|
|
316
382
|
if (field.type === 'custom' && typeof field.editor === 'function') {
|
|
317
383
|
const renderVersion = this._renderVersion;
|
|
318
384
|
queueMicrotask(() => {
|
|
@@ -325,17 +391,50 @@ export class Filter extends Component {
|
|
|
325
391
|
});
|
|
326
392
|
return host;
|
|
327
393
|
}
|
|
394
|
+
const messages = this._valueMessages();
|
|
328
395
|
const values = operator.arity === 'pair' ? (Array.isArray(node.value) ? node.value : [null, null]) : [node.value];
|
|
329
396
|
if (operator.arity === 'many') {
|
|
330
|
-
const control = choiceSelect(field, node, values[0], true, this._blocked());
|
|
397
|
+
const control = choiceSelect(field, node, values[0], true, this._blocked(), 0, messages);
|
|
331
398
|
if (control) return h('div', { class: 'zx-filter__value' }, control);
|
|
332
|
-
return h('div', { class: 'zx-filter__value' }, valueInput(field, node.id, Array.isArray(node.value) ? node.value.join(', ') : '', 0, this._blocked(), true));
|
|
399
|
+
return h('div', { class: 'zx-filter__value' }, valueInput(field, node.id, Array.isArray(node.value) ? node.value.join(', ') : '', 0, this._blocked(), true, messages));
|
|
333
400
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
401
|
+
const choices = field.choices?.length ? field.choices : field.type === 'boolean' ? [{value: true, label: messages.yes}, {value: false, label: messages.no}] : null;
|
|
402
|
+
const controls = values.map((value, index) => this.options.searchable && choices
|
|
403
|
+
? this._searchSelect(choices, value, operator.arity === 'pair' ? messages.valueAt(field.label, index + 1) : messages.value(field.label), node.id, 'value', (selected) => {
|
|
404
|
+
const next = operator.arity === 'pair' ? (Array.isArray(node.value) ? [...node.value] : [null, null]) : null;
|
|
405
|
+
if (next) next[index] = selected;
|
|
406
|
+
this._setNodeValue(node.id, next ?? selected);
|
|
407
|
+
}) : choiceSelect(field, node, value, false, this._blocked(), index, messages)
|
|
408
|
+
?? valueInput(field, node.id, value, index, this._blocked(), false, messages));
|
|
336
409
|
return h('div', { class: 'zx-filter__value', dataset: { arity: operator.arity } }, controls);
|
|
337
410
|
}
|
|
338
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Owns a themed picker and defers mutation until Select finishes its selection event.
|
|
414
|
+
* @param {Array<{value:unknown,label:string}>} items Choices.
|
|
415
|
+
* @param {unknown} value Selected value.
|
|
416
|
+
* @param {string} label Accessible name.
|
|
417
|
+
* @param {string} nodeId Condition ID.
|
|
418
|
+
* @param {string} part Focus target name.
|
|
419
|
+
* @param {(value:unknown)=>void} changed Selection handler.
|
|
420
|
+
* @returns {HTMLElement} Picker root.
|
|
421
|
+
*/
|
|
422
|
+
_searchSelect(items, value, label, nodeId, part, changed) {
|
|
423
|
+
const version = this._renderVersion;
|
|
424
|
+
const select = new Select(null, {
|
|
425
|
+
items, valueKey: 'value', labelKey: 'label', value, label, filter: 'local',
|
|
426
|
+
disabled: this._blocked(), placeholder: this._message('filter.choosePart', 'Choose %1…', part),
|
|
427
|
+
onchange: ({detail}) => queueMicrotask(() => {
|
|
428
|
+
if (!this._disposed && version === this._renderVersion && this._canMutate()) changed(detail.value);
|
|
429
|
+
})
|
|
430
|
+
});
|
|
431
|
+
this._selectEditors.push(select);
|
|
432
|
+
select.el.classList.add('zx-filter__picker');
|
|
433
|
+
select.refs.input.dataset.filterFocus = part;
|
|
434
|
+
select.refs.input.dataset.nodeId = nodeId;
|
|
435
|
+
return /** @type {HTMLElement} */ (select.el);
|
|
436
|
+
}
|
|
437
|
+
|
|
339
438
|
/** @param {Event} event @returns {void} */
|
|
340
439
|
_handleChange(event) {
|
|
341
440
|
if (!this._canMutate()) return;
|
|
@@ -358,7 +457,7 @@ export class Filter extends Component {
|
|
|
358
457
|
queueMicrotask(() => /** @type {HTMLElement|null} */ (this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`))?.focus());
|
|
359
458
|
} else if (target.dataset.filterLogic !== undefined) {
|
|
360
459
|
const node = findNode(this._value.root, id);
|
|
361
|
-
if (node?.kind === 'group') node.logic = target.value === 'or' ? 'or' : 'and';
|
|
460
|
+
if (node?.kind === 'group' && !(node === this._value.root && this.options.rootLogic)) node.logic = target.value === 'or' ? 'or' : 'and';
|
|
362
461
|
this._changed('update', id);
|
|
363
462
|
} else if (target.dataset.filterValue !== undefined) this._valueInput(target);
|
|
364
463
|
}
|
|
@@ -469,7 +568,10 @@ export class Filter extends Component {
|
|
|
469
568
|
/** @returns {void} */
|
|
470
569
|
_abortAll() { for (const id of [...this._async.keys()]) this._abort(id); }
|
|
471
570
|
/** @returns {void} */
|
|
472
|
-
_destroyEditors() {
|
|
571
|
+
_destroyEditors() {
|
|
572
|
+
for (const editor of [...(this._customEditors ?? []), ...(this._selectEditors ?? [])]) editor.destroy?.();
|
|
573
|
+
this._customEditors = []; this._selectEditors = [];
|
|
574
|
+
}
|
|
473
575
|
/** @returns {boolean} */
|
|
474
576
|
_blocked() { return this._readonly || this._disabled; }
|
|
475
577
|
/** @returns {boolean} */
|
|
@@ -486,37 +588,82 @@ export class Filter extends Component {
|
|
|
486
588
|
_defaultOperator(field) { return field?.defaultOperator && this._operatorsFor(field).some((operator) => operator.id === field.defaultOperator) ? field.defaultOperator : this._operatorsFor(field)[0]?.id ?? null; }
|
|
487
589
|
/** @param {string} text @returns {void} */
|
|
488
590
|
_announce(text) { if (this.refs.status) this.refs.status.textContent = text; }
|
|
591
|
+
|
|
592
|
+
/** Names a field or operator a saved expression refers to that this instance does not offer.
|
|
593
|
+
* @param {string} id Unknown field or operator ID. @returns {string} */
|
|
594
|
+
_unavailable(id) { return this._message('filter.unavailable', 'Unavailable: %1', id); }
|
|
595
|
+
|
|
596
|
+
/** @param {any} node Condition. @param {FilterField|null} field Resolved field. @returns {HTMLElement} */
|
|
597
|
+
_removeConditionButton(node, field) {
|
|
598
|
+
const label = this._message('filter.remove', 'Remove %1',
|
|
599
|
+
field?.label ?? this._message('filter.filterNoun', 'filter'));
|
|
600
|
+
return actionButton('remove', node.id, 'trash', label,
|
|
601
|
+
{ disabled: this._blocked(), ariaLabel: label });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Resolves the strings the two value builders need. They are module functions so that the
|
|
606
|
+
* markup they produce stays testable without a component, which means the words have to be
|
|
607
|
+
* handed to them rather than looked up inside.
|
|
608
|
+
* @returns {FilterValueMessages}
|
|
609
|
+
*/
|
|
610
|
+
_valueMessages() {
|
|
611
|
+
return {
|
|
612
|
+
choose: this._message('filter.choose', 'Choose…'),
|
|
613
|
+
yes: this._message('filter.booleanTrue', 'True'),
|
|
614
|
+
no: this._message('filter.booleanFalse', 'False'),
|
|
615
|
+
commaSeparated: this._message('filter.commaSeparated', 'Comma-separated values'),
|
|
616
|
+
value: (label) => this._message('filter.fieldValue', '%1 value', label),
|
|
617
|
+
valueAt: (label, position) => this._message('filter.fieldValueAt', '%1 value %2', label, position)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Resolves a message through the host translator, falling back to the built-in English text.
|
|
623
|
+
* @param {string} key Message key.
|
|
624
|
+
* @param {string} fallback Built-in text, with `%1`-style placeholders.
|
|
625
|
+
* @param {...unknown} args Interpolation values.
|
|
626
|
+
* @returns {string}
|
|
627
|
+
*/
|
|
628
|
+
_message(key, fallback, ...args) {
|
|
629
|
+
const message = this.msg(key, ...args);
|
|
630
|
+
return message === key ? printf(fallback, args) : message;
|
|
631
|
+
}
|
|
489
632
|
}
|
|
490
633
|
|
|
491
|
-
/** @param {string} action @param {string} id @param {string} iconName @param {string} label @param {Record<string,any>} [props={}] @returns {HTMLElement} */
|
|
634
|
+
/** @param {string} action @param {string} id @param {string} iconName @param {string} label @param {Record<string,any>} [props={}] `kind` picks the Button kind; the rest are element properties. @returns {HTMLElement} */
|
|
492
635
|
function actionButton(action, id, iconName, label, props = {}) {
|
|
493
|
-
|
|
636
|
+
const { kind, ...rest } = props;
|
|
637
|
+
return h('button', {
|
|
638
|
+
type: 'button', class: 'zx-btn', ...rest,
|
|
639
|
+
dataset: { filterAction: action, nodeId: id, size: 'sm', ...(kind ? { kind } : {}) }
|
|
640
|
+
}, icon(iconName), h('span', {}, label));
|
|
494
641
|
}
|
|
495
642
|
|
|
496
|
-
/** @param {FilterField} field @param {any} node @param {any} value @param {boolean} multiple @param {boolean} disabled @param {number} [index=0] @returns {HTMLSelectElement|null} */
|
|
497
|
-
function choiceSelect(field, node, value, multiple, disabled, index = 0) {
|
|
643
|
+
/** @param {FilterField} field @param {any} node @param {any} value @param {boolean} multiple @param {boolean} disabled @param {number} [index=0] @param {FilterValueMessages} [messages=BUILT_IN_VALUE_MESSAGES] @returns {HTMLSelectElement|null} */
|
|
644
|
+
function choiceSelect(field, node, value, multiple, disabled, index = 0, messages = BUILT_IN_VALUE_MESSAGES) {
|
|
498
645
|
const choices = Array.isArray(field.choices) && field.choices.length
|
|
499
646
|
? field.choices
|
|
500
647
|
: field.type === 'boolean'
|
|
501
|
-
? [{ value: true, label:
|
|
648
|
+
? [{ value: true, label: messages.yes }, { value: false, label: messages.no }]
|
|
502
649
|
: null;
|
|
503
650
|
if (!choices) return null;
|
|
504
651
|
const values = new Set(Array.isArray(value) ? value.map(String) : [String(value ?? '')]);
|
|
505
652
|
return /** @type {HTMLSelectElement} */ (h('select', {
|
|
506
653
|
dataset: { filterValue: '', nodeId: node.id, valueIndex: String(index) },
|
|
507
|
-
ariaLabel:
|
|
508
|
-
}, !multiple ? h('option', { value: '', selected: !values.has(String(value)) || value == null },
|
|
654
|
+
ariaLabel: messages.value(field.label), multiple, disabled
|
|
655
|
+
}, !multiple ? h('option', { value: '', selected: !values.has(String(value)) || value == null }, messages.choose) : null,
|
|
509
656
|
choices.map((choice) => h('option', { value: choice.value, selected: values.has(String(choice.value)) }, choice.label))));
|
|
510
657
|
}
|
|
511
658
|
|
|
512
|
-
/** @param {FilterField} field @param {string} id @param {any} value @param {number} index @param {boolean} disabled @param {boolean} [many=false] @returns {HTMLElement} */
|
|
513
|
-
function valueInput(field, id, value, index, disabled, many = false) {
|
|
659
|
+
/** @param {FilterField} field @param {string} id @param {any} value @param {number} index @param {boolean} disabled @param {boolean} [many=false] @param {FilterValueMessages} [messages=BUILT_IN_VALUE_MESSAGES] @returns {HTMLElement} */
|
|
660
|
+
function valueInput(field, id, value, index, disabled, many = false, messages = BUILT_IN_VALUE_MESSAGES) {
|
|
514
661
|
const type = many ? 'text' : ['number', 'money'].includes(field.type) ? 'number' : field.type === 'date' ? 'date' : field.type === 'datetime' ? 'datetime-local' : 'text';
|
|
515
662
|
const listId = field.loadChoices ? `zx-filter-choices-${id}-${index}` : null;
|
|
516
663
|
const input = /** @type {HTMLInputElement} */ (h('input', {
|
|
517
664
|
type, value: value ?? '', dataset: { filterValue: '', nodeId: id, valueIndex: String(index) },
|
|
518
|
-
ariaLabel:
|
|
519
|
-
placeholder: many ?
|
|
665
|
+
ariaLabel: index ? messages.valueAt(field.label, index + 1) : messages.value(field.label), disabled,
|
|
666
|
+
placeholder: many ? messages.commaSeparated : '', list: listId
|
|
520
667
|
}));
|
|
521
668
|
return h('span', { class: 'zx-filter__value-control' },
|
|
522
669
|
input, listId ? h('datalist', { id: listId }) : null);
|
|
@@ -552,3 +699,14 @@ function cssEscape(value) { return globalThis.CSS?.escape ? CSS.escape(value) :
|
|
|
552
699
|
/** @event Filter#query @type {CustomEvent<Record<string,unknown>>} */
|
|
553
700
|
/** @event Filter#loaded @type {CustomEvent<Record<string,unknown>>} */
|
|
554
701
|
/** @event Filter#error @type {CustomEvent<Record<string,unknown>>} */
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Reports a restricted root mismatch without mutating the supplied expression.
|
|
705
|
+
* @param {FilterAst} value Expression to validate.
|
|
706
|
+
* @param {'and'|'or'|null} logic Required root logic.
|
|
707
|
+
* @returns {Record<string,string>|null} Actionable validation error, or null.
|
|
708
|
+
*/
|
|
709
|
+
export function rootLogicError(value, logic) {
|
|
710
|
+
if (!logic || value.root.logic === logic) return null;
|
|
711
|
+
return {nodeId: value.root.id, code: 'root-logic', message: `This filter requires match ${logic === 'or' ? 'any' : 'all'} conditions.`};
|
|
712
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
.zx-filter-panel {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: var(--zx-space-4);
|
|
4
|
+
align-content: start;
|
|
5
|
+
min-inline-size: 0;
|
|
6
|
+
color: var(--zx-color-text);
|
|
7
|
+
font-size: var(--zx-control-font-size);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.zx-filter-panel__fields {
|
|
11
|
+
display: grid;
|
|
12
|
+
/* One column in a sheet or a sidebar, more when the panel sits above a list and has the room. */
|
|
13
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
14
|
+
align-items: start;
|
|
15
|
+
gap: var(--zx-space-4);
|
|
16
|
+
min-inline-size: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.zx-filter-panel__field {
|
|
20
|
+
display: grid;
|
|
21
|
+
gap: var(--zx-space-2);
|
|
22
|
+
align-content: start;
|
|
23
|
+
min-inline-size: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.zx-filter-panel__label {
|
|
27
|
+
color: var(--zx-color-text-muted);
|
|
28
|
+
font-size: var(--zx-text-sm);
|
|
29
|
+
font-weight: var(--zx-weight-medium);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.zx-filter-panel__input {
|
|
33
|
+
inline-size: 100%;
|
|
34
|
+
min-inline-size: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.zx-filter-panel__range {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
40
|
+
gap: var(--zx-space-2);
|
|
41
|
+
min-inline-size: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.zx-filter-panel__edge {
|
|
45
|
+
display: grid;
|
|
46
|
+
gap: var(--zx-space-1);
|
|
47
|
+
min-inline-size: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.zx-filter-panel__edge-label {
|
|
51
|
+
color: var(--zx-color-text-muted);
|
|
52
|
+
font-size: var(--zx-text-xs);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.zx-filter-panel__unsupported {
|
|
56
|
+
margin: 0;
|
|
57
|
+
border: 1px dashed var(--zx-color-border-strong);
|
|
58
|
+
border-radius: var(--zx-radius-md);
|
|
59
|
+
background: var(--zx-color-bg-muted);
|
|
60
|
+
color: var(--zx-color-text-muted);
|
|
61
|
+
padding: var(--zx-space-2) var(--zx-space-3);
|
|
62
|
+
font-size: var(--zx-text-sm);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.zx-filter-panel__empty {
|
|
66
|
+
margin: 0;
|
|
67
|
+
border: 1px dashed var(--zx-color-border);
|
|
68
|
+
border-radius: var(--zx-radius-md);
|
|
69
|
+
color: var(--zx-color-text-muted);
|
|
70
|
+
padding: var(--zx-space-5) var(--zx-space-4);
|
|
71
|
+
text-align: center;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.zx-filter-panel__actions {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-wrap: wrap;
|
|
77
|
+
justify-content: end;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--zx-space-2);
|
|
80
|
+
border-block-start: 1px solid var(--zx-color-border);
|
|
81
|
+
padding-block-start: var(--zx-space-3);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Draft mode with unapplied edits: the Apply button is the one thing still to do. */
|
|
85
|
+
.zx-filter-panel[data-state="dirty"] .zx-filter-panel__apply {
|
|
86
|
+
box-shadow: var(--zx-focus-ring);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.zx-filter-panel .zx-checklist {
|
|
90
|
+
min-inline-size: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The composed controls size themselves for a form row. A filter panel is often half that wide —
|
|
94
|
+
a sheet, a sidebar, one of two range edges — so inside it they stretch and are allowed to
|
|
95
|
+
shrink instead of overflowing the column they were given. */
|
|
96
|
+
.zx-filter-panel .zx-number-field {
|
|
97
|
+
display: flex;
|
|
98
|
+
inline-size: 100%;
|
|
99
|
+
min-inline-size: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.zx-filter-panel .zx-number-field__input {
|
|
103
|
+
min-inline-size: 0;
|
|
104
|
+
}
|