@zeyos/zx 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/DESIGN-SYSTEM.md +59 -3
  3. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  4. package/dist/types/components/filter/filter-model.d.ts +87 -0
  5. package/dist/types/components/filter/filter.d.ts +331 -0
  6. package/dist/types/components/select/select.d.ts +46 -4
  7. package/dist/types/components/table/table.d.ts +103 -0
  8. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  9. package/dist/types/core/dom.d.ts +1 -1
  10. package/dist/types/core/util.d.ts +9 -0
  11. package/dist/types/index.d.ts +4 -1
  12. package/dist/types/zeyos/icons.d.ts +21 -4
  13. package/dist/types/zeyos/index.d.ts +1 -1
  14. package/dist/types/zeyos/select.d.ts +2 -0
  15. package/dist/zx-zeyos.esm.js +703 -75
  16. package/dist/zx-zeyos.esm.min.js +1 -1
  17. package/dist/zx.css +564 -9
  18. package/dist/zx.esm.js +1569 -115
  19. package/dist/zx.esm.min.js +3 -3
  20. package/dist/zx.global.js +1569 -115
  21. package/dist/zx.global.min.js +3 -3
  22. package/dist/zx.min.css +1 -1
  23. package/docs/api.json +379 -0
  24. package/docs/llms.md +97 -18
  25. package/docs/llms.txt +11 -9
  26. package/package.json +1 -1
  27. package/src/components/app-icon/app-icon.css +189 -0
  28. package/src/components/app-icon/app-icon.js +137 -0
  29. package/src/components/button/button.css +34 -2
  30. package/src/components/card/card.css +25 -3
  31. package/src/components/filter/filter-model.js +226 -0
  32. package/src/components/filter/filter.css +147 -0
  33. package/src/components/filter/filter.js +554 -0
  34. package/src/components/grid/grid.css +1 -1
  35. package/src/components/grid/grid.js +4 -1
  36. package/src/components/message/message.css +40 -3
  37. package/src/components/select/select.css +17 -0
  38. package/src/components/select/select.js +105 -24
  39. package/src/components/table/table.css +85 -0
  40. package/src/components/table/table.js +418 -24
  41. package/src/components/tag-picker/tag-picker.css +19 -0
  42. package/src/components/tag-picker/tag-picker.js +42 -5
  43. package/src/core/dom.js +1 -1
  44. package/src/core/util.js +20 -0
  45. package/src/index.js +7 -1
  46. package/src/zeyos/icons.js +37 -24
  47. package/src/zeyos/index.js +1 -1
  48. package/src/zeyos/select.js +6 -5
  49. package/styles/tokens/semantic.css +17 -0
  50. package/styles/tokens/themes.css +2 -2
  51. package/styles/zx.css +2 -0
package/dist/zx.esm.js CHANGED
@@ -73,6 +73,20 @@ function isElement(value) {
73
73
  return Boolean(value && typeof value === "object" && /** @type {{nodeType?: unknown}} */
74
74
  value.nodeType === 1);
75
75
  }
76
+ function isCssColor(value) {
77
+ if (typeof value !== "string") return false;
78
+ const text4 = value.trim();
79
+ if (!text4 || text4.length > 256 || /[;{}\u0000-\u001f\u007f]/.test(text4) || /(?:url|image|image-set|cross-fade|element|var)\s*\(/i.test(text4)) return false;
80
+ const supports = globalThis.CSS?.supports;
81
+ if (typeof supports === "function") {
82
+ try {
83
+ return supports.call(globalThis.CSS, "color", text4);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ return /^(?:#[\da-f]{3,8}|[a-z]+|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\([^;{}]+\))$/i.test(text4);
89
+ }
76
90
  function clamp(n, min, max) {
77
91
  return Math.min(max, Math.max(min, n));
78
92
  }
@@ -710,9 +724,9 @@ var Component = class extends EventTarget {
710
724
  let rendered;
711
725
  try {
712
726
  rendered = h.scope(this.refs, () => this.render());
713
- } catch (error) {
727
+ } catch (error2) {
714
728
  this.#abort.abort();
715
- throw error;
729
+ throw error2;
716
730
  }
717
731
  if (this.#created) {
718
732
  const root = isElement(rendered) ? rendered : this.el;
@@ -1003,8 +1017,8 @@ var Field = class _Field extends Component {
1003
1017
  /** @type {Readonly<FieldOptions>} */
1004
1018
  this.options
1005
1019
  ));
1006
- } catch (error) {
1007
- const message = error instanceof Error ? error.message : String(error);
1020
+ } catch (error2) {
1021
+ const message = error2 instanceof Error ? error2.message : String(error2);
1008
1022
  console.warn(`[zx.Field] Adapter "${type}" failed: ${message}`);
1009
1023
  this.adapter = errorAdapter(message, this.controlId);
1010
1024
  }
@@ -4987,8 +5001,8 @@ var FieldUpload = class extends Component {
4987
5001
  return response;
4988
5002
  } catch (caught) {
4989
5003
  if (runId !== this._runId) return null;
4990
- const error = caught instanceof Error ? caught : new Error(String(caught));
4991
- if (error.name === "AbortError") {
5004
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
5005
+ if (error2.name === "AbortError") {
4992
5006
  this._finishAbort();
4993
5007
  return null;
4994
5008
  }
@@ -4997,8 +5011,8 @@ var FieldUpload = class extends Component {
4997
5011
  this._transportController = null;
4998
5012
  this._abort.hidden = true;
4999
5013
  this._setState("error");
5000
- this._status.textContent = error.message;
5001
- this.emit("error", { error });
5014
+ this._status.textContent = error2.message;
5015
+ this.emit("error", { error: error2 });
5002
5016
  return null;
5003
5017
  }
5004
5018
  }
@@ -5146,10 +5160,10 @@ var FieldUpload = class extends Component {
5146
5160
  this.emit("abort", {});
5147
5161
  }
5148
5162
  /** @param {Error} error @returns {void} */
5149
- _selectionError(error) {
5163
+ _selectionError(error2) {
5150
5164
  this._setState("error");
5151
- this._status.textContent = error.message;
5152
- this.emit("error", { error });
5165
+ this._status.textContent = error2.message;
5166
+ this.emit("error", { error: error2 });
5153
5167
  }
5154
5168
  /** @returns {void} */
5155
5169
  _renderPreview() {
@@ -5232,9 +5246,9 @@ function parseResponse(text4) {
5232
5246
  }
5233
5247
  function abortError() {
5234
5248
  if (typeof DOMException === "function") return new DOMException("Upload aborted", "AbortError");
5235
- const error = new Error("Upload aborted");
5236
- error.name = "AbortError";
5237
- return error;
5249
+ const error2 = new Error("Upload aborted");
5250
+ error2.name = "AbortError";
5251
+ return error2;
5238
5252
  }
5239
5253
 
5240
5254
  // src/components/field-upload/field-adapter.js
@@ -6360,8 +6374,8 @@ var Rating = class extends Component {
6360
6374
  const shown = this._preview ?? this._value;
6361
6375
  const symbols = this.refs.symbols.querySelectorAll(".zx-rating__symbol");
6362
6376
  symbols.forEach((symbol, index) => {
6363
- const filled = Math.min(1, Math.max(0, shown - index));
6364
- symbol.style.setProperty("--zx-rating-fill", `${filled * 100}%`);
6377
+ const filled2 = Math.min(1, Math.max(0, shown - index));
6378
+ symbol.style.setProperty("--zx-rating-fill", `${filled2 * 100}%`);
6365
6379
  symbol.toggleAttribute("data-preview", this._preview !== null);
6366
6380
  });
6367
6381
  this.el.dataset.value = String(shown);
@@ -6647,6 +6661,7 @@ var Select = class _Select extends Component {
6647
6661
  static defaults = {
6648
6662
  items: [],
6649
6663
  fixedItems: [],
6664
+ fixedLabel: null,
6650
6665
  valueKey: "ID",
6651
6666
  labelKey: "name",
6652
6667
  renderItem: null,
@@ -6684,8 +6699,7 @@ var Select = class _Select extends Component {
6684
6699
  this._position = null;
6685
6700
  this._createdRoot = false;
6686
6701
  this._root = null;
6687
- this._originalChildren = [];
6688
- this._originalAttributes = /* @__PURE__ */ new Map();
6702
+ this._snapshot = null;
6689
6703
  this._createdRoot = this.el === null;
6690
6704
  const root = (
6691
6705
  /** @type {HTMLElement} */
@@ -6693,10 +6707,7 @@ var Select = class _Select extends Component {
6693
6707
  );
6694
6708
  this.el = root;
6695
6709
  this._root = root;
6696
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6697
- this._rememberAttribute(root, "data-state");
6698
- this._rememberAttribute(root, "data-loading");
6699
- this._rememberAttribute(root, "aria-disabled");
6710
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6700
6711
  const listId = uid("zx-select-list");
6701
6712
  const input = h("input", {
6702
6713
  ref: "input",
@@ -6999,6 +7010,66 @@ var Select = class _Select extends Component {
6999
7010
  select.set(value, { silent: true });
7000
7011
  return select;
7001
7012
  }
7013
+ /**
7014
+ * Creates a rich entity picker with optional Recent choices and a create command.
7015
+ * @param {Element|string|null} target Component target.
7016
+ * @param {SelectOptions & EntitySelectOptions} [options={}] Entity-preset options.
7017
+ * @returns {Select}
7018
+ */
7019
+ static entity(target, options = {}) {
7020
+ const {
7021
+ recent = [],
7022
+ recentLabel = "Recent",
7023
+ create = false,
7024
+ iconKey = "icon",
7025
+ colorKey = "color",
7026
+ subtitleKey = "description",
7027
+ renderIcon = null,
7028
+ fixedItems = [],
7029
+ actions: actions2 = [],
7030
+ renderItem = null,
7031
+ renderValueAdornment = null,
7032
+ ...rest
7033
+ } = options;
7034
+ const visual = (item, selected = false) => {
7035
+ if (typeof renderIcon === "function") return renderIcon(item, { selected });
7036
+ const value = readSelectItem(item, iconKey);
7037
+ if (value && typeof value === "object" && typeof value.nodeType === "number") return value.cloneNode(true);
7038
+ if (typeof value !== "string" || !value) return null;
7039
+ const color = readSelectItem(item, colorKey);
7040
+ return h("span", {
7041
+ class: "zx-select__entity-glyph",
7042
+ style: isCssColor(color) ? { "--zx-select-entity-color": String(color) } : null,
7043
+ ariaHidden: "true"
7044
+ }, icon(value, { size: 14 }));
7045
+ };
7046
+ const createAction = create ? normalizeEntityCreateAction(create) : null;
7047
+ const select = new _Select(target, {
7048
+ ...rest,
7049
+ fixedItems: [...Array.isArray(recent) ? recent : [], ...Array.isArray(fixedItems) ? fixedItems : []],
7050
+ fixedLabel: Array.isArray(recent) && recent.length ? recentLabel : rest.fixedLabel ?? null,
7051
+ actions: [...Array.isArray(actions2) ? actions2 : [], ...createAction ? [createAction] : []],
7052
+ renderItem: renderItem ?? ((item) => {
7053
+ const decoration = visual(item, false);
7054
+ const subtitle = readSelectItem(item, subtitleKey);
7055
+ const copy = h(
7056
+ "span",
7057
+ { class: "zx-select__entity-copy" },
7058
+ h("span", { class: "zx-select__entity-label" }, String(readSelectItem(item, rest.labelKey ?? "name") ?? "")),
7059
+ subtitle == null || subtitle === "" ? null : h("span", { class: "zx-select__entity-subtitle" }, String(subtitle))
7060
+ );
7061
+ return h(
7062
+ "span",
7063
+ { class: "zx-select__entity" },
7064
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
7065
+ copy
7066
+ );
7067
+ }),
7068
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
7069
+ });
7070
+ select.el.dataset.preset = "entity";
7071
+ return select;
7072
+ }
7002
7073
  /**
7003
7074
  * Creates the record-permission picker: the fixed Private and Public choices above the groups a
7004
7075
  * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
@@ -7049,13 +7120,7 @@ var Select = class _Select extends Component {
7049
7120
  /** @type {HTMLElement} */
7050
7121
  this.el
7051
7122
  );
7052
- if (!this._createdRoot && root) {
7053
- root.replaceChildren(...this._originalChildren);
7054
- for (const [name, value] of this._originalAttributes) {
7055
- if (value === null) root.removeAttribute(name);
7056
- else root.setAttribute(name, value);
7057
- }
7058
- }
7123
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
7059
7124
  super.destroy();
7060
7125
  }
7061
7126
  /** @param {KeyboardEvent} event @returns {void} */
@@ -7192,14 +7257,19 @@ var Select = class _Select extends Component {
7192
7257
  list.replaceChildren();
7193
7258
  this._optionEntries = [];
7194
7259
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7260
+ const fixed = this._uniqueItems(this._visibleFixed);
7261
+ const items = this._uniqueItems(this._visibleItems, fixed);
7195
7262
  if (this.options.clearable) {
7196
7263
  this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7197
7264
  }
7198
- for (const item of this._visibleFixed) this._appendOption(item, false);
7199
- if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
7265
+ if (fixed.length > 0 && this.options.fixedLabel) {
7266
+ list.append(h("div", { class: "zx-select__group", role: "presentation" }, String(this.options.fixedLabel)));
7267
+ }
7268
+ for (const item of fixed) this._appendOption(item, false);
7269
+ if (fixed.length > 0 && items.length > 0 && !this.options.groupKey) {
7200
7270
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7201
7271
  }
7202
- for (const item of this._visibleItems) {
7272
+ for (const item of items) {
7203
7273
  if (this.options.groupKey) {
7204
7274
  const group = this._read(item, this.options.groupKey);
7205
7275
  if (!Object.is(group, lastGroup)) {
@@ -7213,7 +7283,7 @@ var Select = class _Select extends Component {
7213
7283
  this._appendOption(item, false);
7214
7284
  }
7215
7285
  if (this._actions.length > 0) {
7216
- if (this._visibleFixed.length > 0 || this._visibleItems.length > 0 || this.options.clearable) {
7286
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7217
7287
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7218
7288
  }
7219
7289
  let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
@@ -7228,7 +7298,7 @@ var Select = class _Select extends Component {
7228
7298
  this._appendAction(action);
7229
7299
  }
7230
7300
  }
7231
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && this._actions.length === 0 && !this._loading) {
7301
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7232
7302
  list.append(h("div", {
7233
7303
  class: "zx-select__empty",
7234
7304
  role: "status"
@@ -7429,6 +7499,16 @@ var Select = class _Select extends Component {
7429
7499
  _matchFixed(query) {
7430
7500
  return matchItems(this._fixedItems, query, this._searchKeys());
7431
7501
  }
7502
+ /** De-duplicates choices by public value while preserving caller order. @param {SelectItem[]} items @param {SelectItem[]} [excluded=[]] @returns {SelectItem[]} */
7503
+ _uniqueItems(items, excluded = []) {
7504
+ const values = excluded.map((item) => this._itemValue(item));
7505
+ return items.filter((item) => {
7506
+ const value = this._itemValue(item);
7507
+ if (values.some((candidate) => Object.is(candidate, value))) return false;
7508
+ values.push(value);
7509
+ return true;
7510
+ });
7511
+ }
7432
7512
  /** @returns {number} */
7433
7513
  _selectedEntryIndex() {
7434
7514
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7440,16 +7520,28 @@ var Select = class _Select extends Component {
7440
7520
  _itemEntryIndex(itemIndex) {
7441
7521
  return itemIndex + (this.options.clearable ? 1 : 0);
7442
7522
  }
7443
- /** @param {string} name @returns {void} */
7444
- _rememberAttribute(element, name) {
7445
- this._originalAttributes.set(name, element.getAttribute(name));
7446
- }
7447
7523
  /** @param {string} key @param {string} fallback @returns {string} */
7448
7524
  _message(key, fallback) {
7449
7525
  const message = this.msg(key);
7450
7526
  return message === key ? fallback : message;
7451
7527
  }
7452
7528
  };
7529
+ function readSelectItem(item, reader) {
7530
+ if (typeof reader === "function") return reader(item);
7531
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
7532
+ return null;
7533
+ }
7534
+ function normalizeEntityCreateAction(value) {
7535
+ const descriptor = value === true ? {} : value;
7536
+ return {
7537
+ id: descriptor.id ?? "create",
7538
+ label: descriptor.label ?? "Create new\u2026",
7539
+ description: descriptor.description,
7540
+ group: descriptor.group ?? "New",
7541
+ icon: descriptor.icon ?? "plus",
7542
+ invoke: descriptor.invoke
7543
+ };
7544
+ }
7453
7545
  function normalizeRenderedContent(rendered) {
7454
7546
  return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7455
7547
  /** @type {Node} */
@@ -7926,6 +8018,8 @@ var TagPicker = class extends Component {
7926
8018
  values: [],
7927
8019
  valueKey: "ID",
7928
8020
  labelKey: "name",
8021
+ iconKey: "icon",
8022
+ colorKey: "color",
7929
8023
  searchKeys: null,
7930
8024
  filter: "local",
7931
8025
  minQuery: 0,
@@ -8266,12 +8360,13 @@ var TagPicker = class extends Component {
8266
8360
  _renderTags() {
8267
8361
  const tags = this._selected.map((item) => {
8268
8362
  const label = this._labelOf(item);
8269
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
8270
- const tag = h(
8271
- "span",
8272
- { class: "zx-tag-picker__tag" },
8273
- h("span", { class: "zx-tag-picker__tag-label" }, content)
8274
- );
8363
+ const color = this._colorOf(item);
8364
+ const content = this.options.renderTag ? this.options.renderTag(item) : [this._iconOf(item), h("span", { class: "zx-tag-picker__tag-label" }, label)];
8365
+ const tag = h("span", {
8366
+ class: "zx-tag-picker__tag",
8367
+ dataset: { colored: String(Boolean(color)) },
8368
+ style: color ? { "--zx-tag-color": color } : null
8369
+ }, content);
8275
8370
  if (!this._readonly && !this._disabled) {
8276
8371
  tag.append(h("button", {
8277
8372
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -8309,7 +8404,7 @@ var TagPicker = class extends Component {
8309
8404
  h(
8310
8405
  "span",
8311
8406
  { class: "zx-tag-picker__option-label" },
8312
- this.options.renderItem ? this.options.renderItem(item) : this._labelOf(item)
8407
+ this.options.renderItem ? this.options.renderItem(item) : [this._iconOf(item), h("span", {}, this._labelOf(item))]
8313
8408
  )
8314
8409
  );
8315
8410
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8534,10 +8629,10 @@ var TagPicker = class extends Component {
8534
8629
  if (sequence !== this._requestSequence) return;
8535
8630
  this._items = Array.isArray(result) ? result.slice() : [];
8536
8631
  this._visible = this._items.slice();
8537
- } catch (error) {
8632
+ } catch (error2) {
8538
8633
  if (sequence !== this._requestSequence) return;
8539
8634
  this._visible = [];
8540
- this.emit("error", { error });
8635
+ this.emit("error", { error: error2 });
8541
8636
  } finally {
8542
8637
  if (sequence === this._requestSequence) {
8543
8638
  this._setLoading(false);
@@ -8630,6 +8725,34 @@ var TagPicker = class extends Component {
8630
8725
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8631
8726
  return String(item ?? "");
8632
8727
  }
8728
+ /** @param {unknown} item @returns {Node|null} */
8729
+ _iconOf(item) {
8730
+ const value = this._readItem(item, this.options.iconKey);
8731
+ const visual = value && typeof value === "object" && typeof value.nodeType === "number" ? (
8732
+ /** @type {Node} */
8733
+ value.cloneNode(true)
8734
+ ) : typeof value === "string" && value.trim() ? icon(value, { size: 12 }) : null;
8735
+ if (!visual) return null;
8736
+ return h("span", {
8737
+ class: "zx-tag-picker__visual",
8738
+ style: this._colorOf(item) ? { "--zx-tag-color": this._colorOf(item) } : null,
8739
+ ariaHidden: "true"
8740
+ }, visual);
8741
+ }
8742
+ /** @param {unknown} item @returns {string|null} */
8743
+ _colorOf(item) {
8744
+ const value = this._readItem(item, this.options.colorKey);
8745
+ return isCssColor(value) ? value : null;
8746
+ }
8747
+ /** @param {unknown} item @param {unknown} reader @returns {unknown} */
8748
+ _readItem(item, reader) {
8749
+ if (typeof reader === "function") return reader(
8750
+ /** @type {TagItem} */
8751
+ item
8752
+ );
8753
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
8754
+ return null;
8755
+ }
8633
8756
  /** @returns {void} */
8634
8757
  _emitChange() {
8635
8758
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9514,38 +9637,38 @@ var Http = class {
9514
9637
  try {
9515
9638
  json2 = JSON.parse(text4);
9516
9639
  } catch (cause) {
9517
- const error = (
9640
+ const error2 = (
9518
9641
  /** @type {Error & {response?: Response}} */
9519
9642
  new Error("Invalid JSON response", { cause })
9520
9643
  );
9521
- error.response = response;
9522
- throw error;
9644
+ error2.response = response;
9645
+ throw error2;
9523
9646
  }
9524
9647
  }
9525
9648
  if (!response.ok) {
9526
9649
  try {
9527
9650
  parseResult(json2);
9528
- } catch (error2) {
9529
- error2.response = response;
9530
- throw error2;
9651
+ } catch (error3) {
9652
+ error3.response = response;
9653
+ throw error3;
9531
9654
  }
9532
- const error = (
9655
+ const error2 = (
9533
9656
  /** @type {Error & {response?: Response}} */
9534
9657
  new Error(`HTTP ${response.status} ${response.statusText}`.trim())
9535
9658
  );
9536
- error.response = response;
9537
- throw error;
9659
+ error2.response = response;
9660
+ throw error2;
9538
9661
  }
9539
9662
  return parseResult(json2);
9540
9663
  } catch (caught) {
9541
- const error = caught instanceof Error ? caught : new Error(String(caught));
9664
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
9542
9665
  if (this.onError) {
9543
9666
  try {
9544
- this.onError(error, { path, options });
9667
+ this.onError(error2, { path, options });
9545
9668
  } catch {
9546
9669
  }
9547
9670
  }
9548
- throw error;
9671
+ throw error2;
9549
9672
  } finally {
9550
9673
  if (timeoutId !== null) clearTimeout(timeoutId);
9551
9674
  }
@@ -10262,9 +10385,9 @@ var Modal = class extends Component {
10262
10385
  this.el.returnValue = "";
10263
10386
  try {
10264
10387
  this._show();
10265
- } catch (error) {
10388
+ } catch (error2) {
10266
10389
  delete this.el.dataset.zxOverlayOrder;
10267
- throw error;
10390
+ throw error2;
10268
10391
  }
10269
10392
  this.el.dataset.state = "open";
10270
10393
  this.emit("open");
@@ -10747,9 +10870,9 @@ var Groupbox = class extends Component {
10747
10870
  }
10748
10871
  try {
10749
10872
  super(normalizedTarget, options);
10750
- } catch (error) {
10873
+ } catch (error2) {
10751
10874
  if (adoption) restoreAdoption(adoption);
10752
- throw error;
10875
+ throw error2;
10753
10876
  }
10754
10877
  this._adoption = adoption;
10755
10878
  }
@@ -11477,7 +11600,11 @@ var Table = class extends Component {
11477
11600
  rowClass: null,
11478
11601
  zebra: true,
11479
11602
  editMode: false,
11480
- hierarchy: false
11603
+ editTrigger: "double",
11604
+ hierarchy: false,
11605
+ rowReorder: false,
11606
+ columnVisibility: false,
11607
+ hiddenColumns: []
11481
11608
  };
11482
11609
  /**
11483
11610
  * Creates a table around a target or creates a new root when target is null.
@@ -11506,6 +11633,7 @@ var Table = class extends Component {
11506
11633
  this._rowElements = /* @__PURE__ */ new Map();
11507
11634
  this._restored = false;
11508
11635
  this._editMode = normalizeEditMode(this.options.editMode);
11636
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
11509
11637
  this._edit = null;
11510
11638
  this._editMessage = null;
11511
11639
  this._stacked = false;
@@ -11517,6 +11645,10 @@ var Table = class extends Component {
11517
11645
  this._rovingCell = null;
11518
11646
  this._rovingMatched = false;
11519
11647
  this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
11648
+ this._hiddenColumns = new Set((Array.isArray(this.options.hiddenColumns) ? this.options.hiddenColumns : []).map(String).filter((id) => this._columns.some((column) => column.id === id)));
11649
+ if (this._hiddenColumns.size >= this._columns.length && this._columns[0]) this._hiddenColumns.delete(this._columns[0].id);
11650
+ this._dragId = null;
11651
+ this._grabbedId = null;
11520
11652
  this._expanded = initialExpanded(this._hierarchy?.expanded);
11521
11653
  if (this._sort && this.options.sortMode === "local") this._sortData();
11522
11654
  if (this._hierarchy?.expanded === true) {
@@ -11538,11 +11670,17 @@ var Table = class extends Component {
11538
11670
  zebra: String(Boolean(this.options.zebra))
11539
11671
  }
11540
11672
  }, this._table);
11673
+ this._rowMoveMessage = this.options.rowReorder ? h("div", {
11674
+ class: "zx-table__reorder-status",
11675
+ role: "status",
11676
+ ariaLive: "polite"
11677
+ }) : null;
11541
11678
  if (this.options.height != null) {
11542
11679
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
11543
11680
  }
11681
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
11544
11682
  if (this._editMode === false) {
11545
- root.replaceChildren(this._scroll);
11683
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
11546
11684
  } else {
11547
11685
  root.dataset.editMode = this._editMode;
11548
11686
  this._editMessage = h("div", {
@@ -11551,7 +11689,7 @@ var Table = class extends Component {
11551
11689
  role: "status",
11552
11690
  ariaLive: "polite"
11553
11691
  });
11554
- root.replaceChildren(this._scroll, this._editMessage);
11692
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
11555
11693
  }
11556
11694
  this._renderColumns();
11557
11695
  this._renderHeader();
@@ -11559,6 +11697,34 @@ var Table = class extends Component {
11559
11697
  this._watchWidth();
11560
11698
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
11561
11699
  this.listen(this._tbody, "dblclick", (event) => this._handleBodyDoubleClick(event));
11700
+ if (this.options.rowReorder) {
11701
+ this.listen(this._tbody, "dragstart", (event) => this._handleRowDragStart(
11702
+ /** @type {DragEvent} */
11703
+ event
11704
+ ));
11705
+ this.listen(this._tbody, "dragover", (event) => this._handleRowDragOver(
11706
+ /** @type {DragEvent} */
11707
+ event
11708
+ ));
11709
+ this.listen(this._tbody, "drop", (event) => this._handleRowDrop(
11710
+ /** @type {DragEvent} */
11711
+ event
11712
+ ));
11713
+ this.listen(this._tbody, "dragend", () => this._clearRowDrag());
11714
+ this.listen(this._tbody, "keydown", (event) => this._handleRowHandleKeydown(
11715
+ /** @type {KeyboardEvent} */
11716
+ event
11717
+ ));
11718
+ }
11719
+ if (this._columnControls) {
11720
+ this.listen(this._columnControls, "change", (event) => {
11721
+ const input = (
11722
+ /** @type {HTMLInputElement|null} */
11723
+ event.target
11724
+ );
11725
+ if (input?.classList.contains("zx-table__column-toggle")) this.setColumnVisible(input.value, input.checked);
11726
+ });
11727
+ }
11562
11728
  if (this._editMode !== false) {
11563
11729
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
11564
11730
  /** @type {KeyboardEvent} */
@@ -11674,6 +11840,81 @@ var Table = class extends Component {
11674
11840
  getData() {
11675
11841
  return [...this._data];
11676
11842
  }
11843
+ /** Returns visible column IDs in caller order. @returns {string[]} */
11844
+ getVisibleColumns() {
11845
+ return this._visibleColumns().map((column) => column.id);
11846
+ }
11847
+ /** Returns hidden column IDs in caller order. @returns {string[]} */
11848
+ getHiddenColumns() {
11849
+ return this._columns.filter((column) => this._hiddenColumns.has(column.id)).map((column) => column.id);
11850
+ }
11851
+ /**
11852
+ * Shows or hides one column. The final visible column cannot be hidden.
11853
+ * @param {string} id Column ID.
11854
+ * @param {boolean} [visible=true] Desired visibility.
11855
+ * @param {{silent?:boolean}} [options={}] Event behavior.
11856
+ * @returns {this}
11857
+ */
11858
+ setColumnVisible(id, visible = true, options = {}) {
11859
+ const column = this._columns.find((candidate) => candidate.id === id);
11860
+ if (!column) throw new RangeError(`Unknown table column: ${id}`);
11861
+ const active = (
11862
+ /** @type {HTMLElement|null} */
11863
+ document.activeElement
11864
+ );
11865
+ const restoreFocus = active?.classList.contains("zx-table__column-toggle") && this._columnList?.contains(active) ? (
11866
+ /** @type {HTMLInputElement} */
11867
+ active.value
11868
+ ) : null;
11869
+ if (!visible && !this._hiddenColumns.has(id) && this._visibleColumns().length <= 1) {
11870
+ this._renderColumnControls(restoreFocus);
11871
+ return this;
11872
+ }
11873
+ const changed = visible ? this._hiddenColumns.delete(id) : !this._hiddenColumns.has(id);
11874
+ if (!visible && changed) this._hiddenColumns.add(id);
11875
+ if (!changed) return this;
11876
+ this._renderColumns();
11877
+ this._renderHeader();
11878
+ this._renderBody();
11879
+ this._renderColumnControls(restoreFocus);
11880
+ if (!options.silent) this.emit("columnvisibilitychange", {
11881
+ visible: this.getVisibleColumns(),
11882
+ hidden: this.getHiddenColumns()
11883
+ });
11884
+ return this;
11885
+ }
11886
+ /** Toggles one column. @param {string} id @param {{silent?:boolean}} [options={}] @returns {this} */
11887
+ toggleColumn(id, options = {}) {
11888
+ return this.setColumnVisible(id, this._hiddenColumns.has(id), options);
11889
+ }
11890
+ /**
11891
+ * Reorders one row relative to another and emits a cancelable `rowmove` before committing.
11892
+ * Hierarchical rows may only move among siblings; an active local sort disables manual order.
11893
+ * @param {unknown} id Moving row ID.
11894
+ * @param {unknown} targetId Target row ID.
11895
+ * @param {'before'|'after'} [position='before'] Target edge.
11896
+ * @returns {this}
11897
+ */
11898
+ moveRow(id, targetId, position2 = "before") {
11899
+ if (!this.options.rowReorder || this._sort && this.options.sortMode === "local") return this;
11900
+ const row2 = this.getRow(id);
11901
+ const target = this.getRow(targetId);
11902
+ if (!row2 || !target || Object.is(id, targetId)) return this;
11903
+ const next = reorderTableRows(this._data, this.options.rowId, id, targetId, position2, this._hierarchy?.parentId);
11904
+ if (!next) return this;
11905
+ const event = this.emit("rowmove", { row: row2, id, target, targetId, position: position2 });
11906
+ if (event.defaultPrevented) return this;
11907
+ this._data = next;
11908
+ this._renderBody();
11909
+ if (this._rowMoveMessage) {
11910
+ const siblings = this._siblingIds(id);
11911
+ const index = siblings.findIndex((candidate) => Object.is(candidate, id));
11912
+ this._rowMoveMessage.textContent = `Moved row to position ${index + 1} of ${siblings.length}.`;
11913
+ }
11914
+ this._emitDataChange();
11915
+ queueMicrotask(() => this._rowHandle(id)?.focus());
11916
+ return this;
11917
+ }
11677
11918
  /**
11678
11919
  * Removes all rows.
11679
11920
  * @returns {this}
@@ -11842,14 +12083,17 @@ var Table = class extends Component {
11842
12083
  */
11843
12084
  startEdit(rowId, columnId) {
11844
12085
  if (this._editMode === false) return this;
11845
- const column = this._columns.find((candidate) => candidate.id === columnId);
11846
- if (!column) return this;
12086
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
12087
+ if (!requested) return this;
11847
12088
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
11848
- if (current === void 0 || resolveEditable(column, current) === false) return this;
12089
+ if (current === void 0) return this;
12090
+ const column = this._editMode === "row" && this._hiddenColumns.has(requested.id) ? this._visibleColumns().find((candidate) => resolveEditable(candidate, current) !== false) ?? null : requested;
12091
+ if (!column || resolveEditable(column, current) === false) return this;
12092
+ const activeColumnId = column.id;
11849
12093
  if (this._edit) {
11850
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
11851
- this._edit.columnId = columnId;
11852
- this._focusEditor(columnId);
12094
+ if (Object.is(this._edit.id, rowId) && this._edit.cells.has(activeColumnId)) {
12095
+ this._edit.columnId = activeColumnId;
12096
+ this._focusEditor(activeColumnId);
11853
12097
  return this;
11854
12098
  }
11855
12099
  this.commitEdit();
@@ -11860,7 +12104,7 @@ var Table = class extends Component {
11860
12104
  const row2 = this._data[index];
11861
12105
  const tr = this._rowElements.get(rowId)?.[0];
11862
12106
  if (!tr) return this;
11863
- const targets = this._editMode === "row" ? this._columns.filter((candidate) => resolveEditable(candidate, row2) !== false) : [column];
12107
+ const targets = this._editMode === "row" ? this._visibleColumns().filter((candidate) => resolveEditable(candidate, row2) !== false) : [column];
11864
12108
  const cells = /* @__PURE__ */ new Map();
11865
12109
  for (const target of targets) {
11866
12110
  const td = cellIn(tr, target.id);
@@ -11869,10 +12113,10 @@ var Table = class extends Component {
11869
12113
  if (entry) cells.set(target.id, entry);
11870
12114
  }
11871
12115
  if (cells.size === 0) return this;
11872
- this._edit = { id: rowId, columnId, row: row2, cells, tr };
12116
+ this._edit = { id: rowId, columnId: activeColumnId, row: row2, cells, tr };
11873
12117
  tr.dataset.editing = "true";
11874
- this.emit("editstart", { row: row2, id: rowId, column, columnId, value: row2?.[columnId] });
11875
- this._focusEditor(columnId);
12118
+ this.emit("editstart", { row: row2, id: rowId, column, columnId: activeColumnId, value: row2?.[activeColumnId] });
12119
+ this._focusEditor(activeColumnId);
11876
12120
  return this;
11877
12121
  }
11878
12122
  /**
@@ -12127,16 +12371,197 @@ var Table = class extends Component {
12127
12371
  }
12128
12372
  super.destroy();
12129
12373
  }
12374
+ /** @returns {TableColumn[]} */
12375
+ _visibleColumns() {
12376
+ return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
12377
+ }
12378
+ /** @returns {HTMLElement} */
12379
+ _createColumnControls() {
12380
+ const list = h("div", { class: "zx-table__column-list" });
12381
+ this._columnList = list;
12382
+ const details = h(
12383
+ "details",
12384
+ { class: "zx-table__column-controls" },
12385
+ h("summary", { class: "zx-button zx-button--sm" }, icon("fields", { size: 13 }), "Columns"),
12386
+ list
12387
+ );
12388
+ this._renderColumnControls();
12389
+ return details;
12390
+ }
12391
+ /** @param {string|null} [focusColumn=null] Column checkbox that keeps focus. @returns {void} */
12392
+ _renderColumnControls(focusColumn = null) {
12393
+ if (!this._columnList) return;
12394
+ const visible = this._visibleColumns();
12395
+ const current = [...this._columnList.querySelectorAll(".zx-table__column-toggle")];
12396
+ const reusable = current.length === this._columns.length && current.every((control, index) => (
12397
+ /** @type {HTMLInputElement} */
12398
+ control.value === this._columns[index].id
12399
+ ));
12400
+ if (reusable) {
12401
+ current.forEach((control, index) => {
12402
+ const input = (
12403
+ /** @type {HTMLInputElement} */
12404
+ control
12405
+ );
12406
+ const checked = !this._hiddenColumns.has(this._columns[index].id);
12407
+ input.checked = checked;
12408
+ input.disabled = checked && visible.length === 1;
12409
+ });
12410
+ return;
12411
+ }
12412
+ this._columnList.replaceChildren(...this._columns.map((column) => {
12413
+ const checked = !this._hiddenColumns.has(column.id);
12414
+ return h(
12415
+ "label",
12416
+ { class: "zx-table__column-option" },
12417
+ h("input", {
12418
+ class: "zx-table__column-toggle",
12419
+ type: "checkbox",
12420
+ value: column.id,
12421
+ checked,
12422
+ disabled: checked && visible.length === 1
12423
+ }),
12424
+ h("span", {}, column.label)
12425
+ );
12426
+ }));
12427
+ if (focusColumn !== null) {
12428
+ const control = [...this._columnList.querySelectorAll(".zx-table__column-toggle")].find((candidate) => (
12429
+ /** @type {HTMLInputElement} */
12430
+ candidate.value === focusColumn
12431
+ ));
12432
+ queueMicrotask(() => {
12433
+ if (control?.isConnected) control.focus();
12434
+ });
12435
+ }
12436
+ }
12437
+ /** @param {DragEvent} event @returns {void} */
12438
+ _handleRowDragStart(event) {
12439
+ const handle = (
12440
+ /** @type {Element|null} */
12441
+ event.target?.closest?.(".zx-table__row-handle")
12442
+ );
12443
+ const rowElement = handle?.closest("tr[data-row]");
12444
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12445
+ if (!handle || !meta || this._sort && this.options.sortMode === "local") {
12446
+ event.preventDefault();
12447
+ return;
12448
+ }
12449
+ this._dragId = meta.id;
12450
+ rowElement.dataset.dragging = "true";
12451
+ event.dataTransfer?.setData("text/plain", "zx-table-row");
12452
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
12453
+ }
12454
+ /** @param {DragEvent} event @returns {void} */
12455
+ _handleRowDragOver(event) {
12456
+ if (this._dragId == null) return;
12457
+ const rowElement = (
12458
+ /** @type {Element|null} */
12459
+ event.target?.closest?.("tr[data-row]")
12460
+ );
12461
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12462
+ if (!meta || Object.is(meta.id, this._dragId) || !this._canMoveTogether(this._dragId, meta.id)) return;
12463
+ event.preventDefault();
12464
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
12465
+ this._clearDropMarkers();
12466
+ const rectangle = rowElement.getBoundingClientRect();
12467
+ rowElement.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
12468
+ }
12469
+ /** @param {DragEvent} event @returns {void} */
12470
+ _handleRowDrop(event) {
12471
+ if (this._dragId == null) return;
12472
+ const rowElement = (
12473
+ /** @type {Element|null} */
12474
+ event.target?.closest?.("tr[data-row]")
12475
+ );
12476
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12477
+ const position2 = rowElement?.dataset.drop === "after" ? "after" : "before";
12478
+ if (meta && this._canMoveTogether(this._dragId, meta.id)) {
12479
+ event.preventDefault();
12480
+ this.moveRow(this._dragId, meta.id, position2);
12481
+ }
12482
+ this._clearRowDrag();
12483
+ }
12484
+ /** @param {KeyboardEvent} event @returns {void} */
12485
+ _handleRowHandleKeydown(event) {
12486
+ const handle = (
12487
+ /** @type {Element|null} */
12488
+ event.target?.closest?.(".zx-table__row-handle")
12489
+ );
12490
+ const rowElement = handle?.closest("tr[data-row]");
12491
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12492
+ if (!handle || !meta) return;
12493
+ if (event.key === " " || event.key === "Enter") {
12494
+ event.preventDefault();
12495
+ this._grabbedId = Object.is(this._grabbedId, meta.id) ? null : meta.id;
12496
+ handle.setAttribute("aria-pressed", String(this._grabbedId != null));
12497
+ const status = this._rowMoveMessage ?? this._editMessage;
12498
+ if (status) status.textContent = this._grabbedId == null ? "Row dropped" : "Row grabbed. Use the arrow keys to move it.";
12499
+ return;
12500
+ }
12501
+ if (event.key === "Escape" && this._grabbedId != null) {
12502
+ event.preventDefault();
12503
+ this._grabbedId = null;
12504
+ handle.setAttribute("aria-pressed", "false");
12505
+ return;
12506
+ }
12507
+ if (!Object.is(this._grabbedId, meta.id) || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
12508
+ event.preventDefault();
12509
+ const siblings = this._siblingIds(meta.id);
12510
+ const index = siblings.findIndex((id) => Object.is(id, meta.id));
12511
+ const targetId = siblings[index + (event.key === "ArrowUp" ? -1 : 1)];
12512
+ if (targetId !== void 0) this.moveRow(meta.id, targetId, event.key === "ArrowUp" ? "before" : "after");
12513
+ }
12514
+ /** @param {unknown} id @returns {unknown[]} */
12515
+ _siblingIds(id) {
12516
+ const row2 = this.getRow(id);
12517
+ if (!row2) return [];
12518
+ const parent = this._hierarchy ? readRowValue(this._hierarchy.parentId, row2) : null;
12519
+ return this._data.filter((candidate) => !this._hierarchy || sameHierarchyParent(readRowValue(this._hierarchy.parentId, candidate), parent)).map((candidate) => this._idFor(candidate));
12520
+ }
12521
+ /** @param {unknown} left @param {unknown} right @returns {boolean} */
12522
+ _canMoveTogether(left, right) {
12523
+ if (!this._hierarchy) return true;
12524
+ const a = this.getRow(left);
12525
+ const b = this.getRow(right);
12526
+ return Boolean(a && b && sameHierarchyParent(
12527
+ readRowValue(this._hierarchy.parentId, a),
12528
+ readRowValue(this._hierarchy.parentId, b)
12529
+ ));
12530
+ }
12531
+ /** @param {unknown} id @returns {HTMLElement|null} */
12532
+ _rowHandle(id) {
12533
+ return (
12534
+ /** @type {HTMLElement|null} */
12535
+ this._rowElements.get(id)?.[0]?.querySelector(".zx-table__row-handle") ?? null
12536
+ );
12537
+ }
12538
+ /** @returns {void} */
12539
+ _clearDropMarkers() {
12540
+ for (const row2 of this._tbody.querySelectorAll("tr[data-drop]")) delete row2.dataset.drop;
12541
+ }
12542
+ /** @returns {void} */
12543
+ _clearRowDrag() {
12544
+ for (const row2 of this._tbody.querySelectorAll("tr[data-dragging]")) delete row2.dataset.dragging;
12545
+ this._clearDropMarkers();
12546
+ this._dragId = null;
12547
+ }
12548
+ /** @returns {string|null} */
12549
+ _hierarchyColumn() {
12550
+ if (!this._hierarchy) return null;
12551
+ return this._visibleColumns().some((column) => column.id === this._hierarchy.column) ? this._hierarchy.column : this._visibleColumns()[0]?.id ?? null;
12552
+ }
12130
12553
  /** @returns {void} */
12131
12554
  _renderColumns() {
12132
12555
  const fragment2 = document.createDocumentFragment();
12556
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
12133
12557
  if (this.options.selectable === "multi") {
12134
12558
  fragment2.append(h("col", { class: "zx-table__selection-column" }));
12135
12559
  }
12136
- const fractions = this._columns.map((column) => parseFraction(column.width));
12560
+ const columns = this._visibleColumns();
12561
+ const fractions = columns.map((column) => parseFraction(column.width));
12137
12562
  const total = fractions.reduce((sum, value) => sum + value, 0);
12138
12563
  this._table.dataset.layout = total > 0 ? "fixed" : "auto";
12139
- this._columns.forEach((column, index) => {
12564
+ columns.forEach((column, index) => {
12140
12565
  const col = h("col");
12141
12566
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
12142
12567
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
@@ -12148,6 +12573,11 @@ var Table = class extends Component {
12148
12573
  _renderHeader() {
12149
12574
  const row2 = h("tr", { class: "zx-table__header-row" });
12150
12575
  if (this._growStep) row2.setAttribute("aria-rowindex", "1");
12576
+ if (this.options.rowReorder) row2.append(h("th", {
12577
+ class: "zx-table__reorder-header",
12578
+ scope: "col",
12579
+ ariaLabel: "Row order"
12580
+ }));
12151
12581
  if (this.options.selectable === "multi") {
12152
12582
  this._selectAll = h("input", {
12153
12583
  class: "zx-table__checkbox zx-table__select-all",
@@ -12173,7 +12603,7 @@ var Table = class extends Component {
12173
12603
  this._selectAll = null;
12174
12604
  }
12175
12605
  this._headers = /* @__PURE__ */ new Map();
12176
- for (const column of this._columns) {
12606
+ for (const column of this._visibleColumns()) {
12177
12607
  const th = h("th", { scope: "col" });
12178
12608
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
12179
12609
  if (align) th.dataset.align = align;
@@ -12213,7 +12643,7 @@ var Table = class extends Component {
12213
12643
  { class: "zx-table__empty-row" },
12214
12644
  h("td", {
12215
12645
  class: "zx-table__empty",
12216
- colspan: this._columns.length + (this.options.selectable === "multi" ? 1 : 0)
12646
+ colspan: this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)
12217
12647
  }, this._emptyContent())
12218
12648
  ));
12219
12649
  } else {
@@ -12222,6 +12652,7 @@ var Table = class extends Component {
12222
12652
  });
12223
12653
  }
12224
12654
  this._tbody.replaceChildren(fragment2);
12655
+ this._table.setAttribute("aria-colcount", String(this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)));
12225
12656
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
12226
12657
  this._syncSelectAll();
12227
12658
  this._syncGrowing();
@@ -12265,6 +12696,19 @@ var Table = class extends Component {
12265
12696
  const elements = this._rowElements.get(id) ?? [];
12266
12697
  elements.push(tr);
12267
12698
  this._rowElements.set(id, elements);
12699
+ if (this.options.rowReorder) {
12700
+ const handle = h("button", {
12701
+ class: "zx-table__row-handle",
12702
+ type: "button",
12703
+ draggable: true,
12704
+ dataset: { rowHandle: "" },
12705
+ ariaLabel: `Move row ${index + 1}`,
12706
+ ariaPressed: String(Object.is(this._grabbedId, id)),
12707
+ disabled: Boolean(this._sort && this.options.sortMode === "local"),
12708
+ title: this._sort && this.options.sortMode === "local" ? "Clear sorting to reorder rows" : null
12709
+ }, icon("drag", { size: 14 }));
12710
+ tr.append(h("td", { class: "zx-table__reorder-cell" }, handle));
12711
+ }
12268
12712
  if (this.options.selectable === "multi") {
12269
12713
  const checkbox = h("input", {
12270
12714
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -12274,7 +12718,7 @@ var Table = class extends Component {
12274
12718
  });
12275
12719
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
12276
12720
  }
12277
- this._columns.forEach((column) => {
12721
+ this._visibleColumns().forEach((column) => {
12278
12722
  const cell = (
12279
12723
  /** @type {HTMLTableCellElement} */
12280
12724
  h("td")
@@ -12310,7 +12754,7 @@ var Table = class extends Component {
12310
12754
  const content = h("span", { class: "zx-table__cell-content" });
12311
12755
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
12312
12756
  else if (value != null) content.append(document.createTextNode(String(value)));
12313
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
12757
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
12314
12758
  cell.replaceChildren(...content.childNodes);
12315
12759
  return;
12316
12760
  }
@@ -12341,6 +12785,7 @@ var Table = class extends Component {
12341
12785
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12342
12786
  const meta = this._rowMeta.get(rowElement);
12343
12787
  if (!meta) return;
12788
+ if (target.closest(".zx-table__row-handle")) return;
12344
12789
  if (this._edit) {
12345
12790
  this.commitEdit();
12346
12791
  if (this._edit) return;
@@ -12360,6 +12805,14 @@ var Table = class extends Component {
12360
12805
  );
12361
12806
  return;
12362
12807
  }
12808
+ if (isInteractiveCellContent(target)) return;
12809
+ if (this._editMode !== false && this._editTrigger === "single") {
12810
+ const cell = target?.closest('td[data-editable="true"]');
12811
+ if (cell && this._tbody.contains(cell)) {
12812
+ this.startEdit(meta.id, String(cell.dataset.column));
12813
+ return;
12814
+ }
12815
+ }
12363
12816
  this.emit("rowclick", { ...meta, event });
12364
12817
  if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
12365
12818
  this._selected = /* @__PURE__ */ new Set([meta.id]);
@@ -12380,7 +12833,8 @@ var Table = class extends Component {
12380
12833
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12381
12834
  const meta = this._rowMeta.get(rowElement);
12382
12835
  if (!meta) return;
12383
- if (this._editMode !== false) {
12836
+ if (isInteractiveCellContent(target)) return;
12837
+ if (this._editMode !== false && this._editTrigger === "double") {
12384
12838
  const cell = target?.closest('td[data-editable="true"]');
12385
12839
  if (cell && this._tbody.contains(cell)) {
12386
12840
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -12682,13 +13136,14 @@ var Table = class extends Component {
12682
13136
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
12683
13137
  */
12684
13138
  _nextEditableCell(rowIndex, columnId, direction) {
12685
- const columnCount = this._columns.length;
13139
+ const columns = this._visibleColumns();
13140
+ const columnCount = columns.length;
12686
13141
  if (rowIndex < 0 || columnCount === 0) return null;
12687
13142
  const visible = this._visibleData();
12688
13143
  const startingId = this._idFor(this._data[rowIndex]);
12689
13144
  let row2 = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
12690
13145
  if (row2 < 0) return null;
12691
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
13146
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
12692
13147
  const limit = visible.length * columnCount + columnCount;
12693
13148
  for (let step = 0; step < limit; step += 1) {
12694
13149
  column += direction;
@@ -12701,7 +13156,7 @@ var Table = class extends Component {
12701
13156
  }
12702
13157
  if (row2 < 0 || row2 >= visible.length) return null;
12703
13158
  const candidateRow = visible[row2];
12704
- const candidateColumn = this._columns[column];
13159
+ const candidateColumn = columns[column];
12705
13160
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
12706
13161
  return {
12707
13162
  id: this._idFor(candidateRow),
@@ -12944,6 +13399,53 @@ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
12944
13399
  }
12945
13400
  return result;
12946
13401
  }
13402
+ function reorderTableRows(rows, rowId, id, targetId, position2 = "before", parentId = null) {
13403
+ if (!Array.isArray(rows) || Object.is(id, targetId)) return null;
13404
+ const from = rows.findIndex((row2) => Object.is(readRowValue(rowId, row2), id));
13405
+ const target = rows.findIndex((row2) => Object.is(readRowValue(rowId, row2), targetId));
13406
+ if (from < 0 || target < 0) return null;
13407
+ if (parentId && !sameHierarchyParent(
13408
+ readRowValue(parentId, rows[from]),
13409
+ readRowValue(parentId, rows[target])
13410
+ )) return null;
13411
+ if (parentId) {
13412
+ const forest = hierarchyForest(rows, rowId, parentId);
13413
+ const movingNode = forest.nodes[from];
13414
+ const targetNode = forest.nodes[target];
13415
+ if (!movingNode || !targetNode || movingNode.parent !== targetNode.parent) return null;
13416
+ const branchNodes = (root) => {
13417
+ const result = [];
13418
+ const stack2 = [root];
13419
+ while (stack2.length) {
13420
+ const node = (
13421
+ /** @type {TableHierarchyNode} */
13422
+ stack2.pop()
13423
+ );
13424
+ result.push(node);
13425
+ for (let index = node.children.length - 1; index >= 0; index -= 1) stack2.push(node.children[index]);
13426
+ }
13427
+ return result;
13428
+ };
13429
+ const moving2 = new Set(branchNodes(movingNode).map((node) => node.index));
13430
+ const targetBranch = new Set(branchNodes(targetNode).map((node) => node.index));
13431
+ const block = rows.filter((_row, index) => moving2.has(index));
13432
+ const remaining = rows.filter((_row, index) => !moving2.has(index));
13433
+ const remainingSourceIndexes = rows.map((_row, index) => index).filter((index) => !moving2.has(index));
13434
+ let insertion = remainingSourceIndexes.findIndex((index) => index === target);
13435
+ if (position2 === "after") {
13436
+ const targetPositions = remainingSourceIndexes.map((sourceIndex, index) => targetBranch.has(sourceIndex) ? index : -1).filter((index) => index >= 0);
13437
+ insertion = Math.max(...targetPositions) + 1;
13438
+ }
13439
+ if (insertion < 0) return null;
13440
+ remaining.splice(insertion, 0, ...block);
13441
+ return remaining;
13442
+ }
13443
+ const next = [...rows];
13444
+ const [moving] = next.splice(from, 1);
13445
+ const adjustedTarget = next.findIndex((row2) => Object.is(readRowValue(rowId, row2), targetId));
13446
+ next.splice(adjustedTarget + (position2 === "after" ? 1 : 0), 0, moving);
13447
+ return next;
13448
+ }
12947
13449
  function resolveEditable(column, row2) {
12948
13450
  if (!column || typeof column !== "object") return false;
12949
13451
  if (typeof column.editor === "function") {
@@ -13024,7 +13526,8 @@ function hierarchyForest(rows, rowId, parentId) {
13024
13526
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
13025
13527
  const parentOf = /* @__PURE__ */ new Map();
13026
13528
  for (const node of nodes) {
13027
- const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
13529
+ const parentValue = readRowValue(parentId, node.row);
13530
+ const parent = parentValue == null ? null : byId.get(parentValue) ?? null;
13028
13531
  parentOf.set(node, parent === node ? null : parent);
13029
13532
  }
13030
13533
  for (const node of nodes) {
@@ -13053,6 +13556,9 @@ function hierarchyBranchIds(rows, rowId, parentId) {
13053
13556
  function readRowValue(accessor, row2) {
13054
13557
  return typeof accessor === "function" ? accessor(row2) : row2?.[accessor];
13055
13558
  }
13559
+ function sameHierarchyParent(left, right) {
13560
+ return left == null && right == null || Object.is(left, right);
13561
+ }
13056
13562
  function normalizeHierarchy(option, columns) {
13057
13563
  if (!option || typeof option !== "object") return null;
13058
13564
  const parentId = option.parentId;
@@ -13072,6 +13578,23 @@ function normalizeEditMode(mode) {
13072
13578
  function isInsideEditor(target) {
13073
13579
  return Boolean(target?.closest?.(".zx-table__editor"));
13074
13580
  }
13581
+ function isInteractiveCellContent(target) {
13582
+ const cell = target?.closest?.("td[data-column]");
13583
+ const interactive = target?.closest?.([
13584
+ "a[href]",
13585
+ "button",
13586
+ "input",
13587
+ "select",
13588
+ "textarea",
13589
+ "summary",
13590
+ '[contenteditable="true"]',
13591
+ '[role="button"]',
13592
+ '[role="link"]',
13593
+ '[tabindex]:not([tabindex="-1"])',
13594
+ "[data-zx-table-no-edit]"
13595
+ ].join(","));
13596
+ return Boolean(cell && interactive && interactive !== cell && cell.contains(interactive));
13597
+ }
13075
13598
  function deferToEditor(target) {
13076
13599
  if (target.closest("[popover]")) return true;
13077
13600
  return target.getAttribute?.("aria-expanded") === "true";
@@ -15560,11 +16083,11 @@ var Card = class extends Component {
15560
16083
  this.setFooter(this.options.footer);
15561
16084
  this.setLink(this.options.link);
15562
16085
  return root;
15563
- } catch (error) {
16086
+ } catch (error2) {
15564
16087
  this._actionAbort?.abort();
15565
16088
  this._linkAbort?.abort();
15566
16089
  if (!created) restoreTarget(root, this._original);
15567
- throw error;
16090
+ throw error2;
15568
16091
  }
15569
16092
  }
15570
16093
  /** Replaces the card title. @param {string} title Next title. @returns {this} */
@@ -15608,9 +16131,9 @@ var Card = class extends Component {
15608
16131
  if (onclick) control.addEventListener("click", onclick, { signal: nextAbort.signal });
15609
16132
  return control;
15610
16133
  });
15611
- } catch (error) {
16134
+ } catch (error2) {
15612
16135
  nextAbort.abort();
15613
- throw error;
16136
+ throw error2;
15614
16137
  }
15615
16138
  this._actionAbort?.abort();
15616
16139
  this._actionAbort = nextAbort;
@@ -19051,6 +19574,107 @@ function count(value) {
19051
19574
  return value.toLocaleString();
19052
19575
  }
19053
19576
 
19577
+ // src/components/app-icon/app-icon.js
19578
+ var AppIcon = class extends Component {
19579
+ static cssName = "app-icon";
19580
+ /** @type {Readonly<AppIconOptions>} */
19581
+ static defaults = {
19582
+ icon: null,
19583
+ color: null,
19584
+ size: 36,
19585
+ iconSize: "52%",
19586
+ label: null,
19587
+ badge: null,
19588
+ selected: false,
19589
+ glass: "subtle",
19590
+ class: []
19591
+ };
19592
+ /** @returns {HTMLElement} */
19593
+ render() {
19594
+ this._createdRoot = this.el === null;
19595
+ const root = (
19596
+ /** @type {HTMLElement} */
19597
+ this.el ?? h("span")
19598
+ );
19599
+ this.el = root;
19600
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
19601
+ this._state = { ...this.options };
19602
+ this._appliedClasses = [];
19603
+ this._sync();
19604
+ return root;
19605
+ }
19606
+ /** Updates identity or presentation without replacing the root. @param {Partial<AppIconOptions>} values @returns {this} */
19607
+ set(values = {}) {
19608
+ if (!values || typeof values !== "object") return this;
19609
+ Object.assign(this._state, values);
19610
+ this._sync();
19611
+ return this;
19612
+ }
19613
+ /** Restores an enhanced target. @returns {void} */
19614
+ destroy() {
19615
+ const root = this.el;
19616
+ const snapshot10 = this._snapshot;
19617
+ super.destroy();
19618
+ if (!this._createdRoot && root) restoreTarget(root, snapshot10);
19619
+ }
19620
+ /** @returns {void} */
19621
+ _sync() {
19622
+ const state = this._state;
19623
+ this.el.classList.remove(...this._appliedClasses);
19624
+ this._appliedClasses = [state.class].flat().flatMap((value) => String(value ?? "").split(/\s+/)).filter(Boolean);
19625
+ this.el.classList.add(...this._appliedClasses);
19626
+ const size2 = cssLength2(state.size, "36px");
19627
+ const glyphSize = cssLength2(state.iconSize, "52%");
19628
+ this.el.style.setProperty("--zx-app-icon-size", size2);
19629
+ this.el.style.setProperty("--zx-app-icon-glyph-size", glyphSize);
19630
+ if (isCssColor(state.color)) this.el.style.setProperty("--zx-app-icon-color", String(state.color));
19631
+ else this.el.style.removeProperty("--zx-app-icon-color");
19632
+ this.el.dataset.glass = normalizeGlass(state.glass);
19633
+ this.el.toggleAttribute("data-selected", Boolean(state.selected));
19634
+ const label = state.label == null ? "" : String(state.label).trim();
19635
+ if (label) {
19636
+ this.el.setAttribute("role", "img");
19637
+ this.el.setAttribute("aria-label", label);
19638
+ this.el.removeAttribute("aria-hidden");
19639
+ } else {
19640
+ this.el.removeAttribute("role");
19641
+ this.el.removeAttribute("aria-label");
19642
+ this.el.setAttribute("aria-hidden", "true");
19643
+ }
19644
+ const visual = resolveVisual(state.icon);
19645
+ const badge2 = state.badge == null || state.badge === "" ? null : h("span", {
19646
+ class: "zx-app-icon__badge",
19647
+ ariaHidden: "true"
19648
+ }, String(state.badge));
19649
+ const children = [
19650
+ h("span", { class: "zx-app-icon__shine", ariaHidden: "true" }),
19651
+ h("span", { class: "zx-app-icon__glyph", ariaHidden: "true" }, visual)
19652
+ ];
19653
+ if (badge2) children.push(badge2);
19654
+ this.el.replaceChildren(...children);
19655
+ }
19656
+ };
19657
+ function appIcon(options = {}) {
19658
+ return new AppIcon(null, options).toElement();
19659
+ }
19660
+ function resolveVisual(value) {
19661
+ const resolved = typeof value === "function" ? value() : value;
19662
+ if (typeof resolved === "string") return icon(resolved, { size: "1em" });
19663
+ return resolved && typeof resolved === "object" && typeof resolved.nodeType === "number" ? (
19664
+ /** @type {Node} */
19665
+ resolved
19666
+ ) : null;
19667
+ }
19668
+ function cssLength2(value, fallback) {
19669
+ if (typeof value === "number" && Number.isFinite(value)) return `${Math.max(1, value)}px`;
19670
+ const text4 = String(value ?? "").trim();
19671
+ return text4 && !/[;{}]/.test(text4) ? text4 : fallback;
19672
+ }
19673
+ function normalizeGlass(value) {
19674
+ if (value === false || value === "none") return "none";
19675
+ return value === "strong" ? "strong" : "subtle";
19676
+ }
19677
+
19054
19678
  // src/components/launcher/launcher.js
19055
19679
  var Launcher = class extends Component {
19056
19680
  static cssName = "launcher";
@@ -19320,9 +19944,9 @@ var Launcher = class extends Component {
19320
19944
  itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
19321
19945
  })));
19322
19946
  this._renderResults();
19323
- } catch (error) {
19947
+ } catch (error2) {
19324
19948
  if (request.signal.aborted || sequence !== this._requestSequence) return;
19325
- this.emit("error", { source, error });
19949
+ this.emit("error", { source, error: error2 });
19326
19950
  }
19327
19951
  }));
19328
19952
  if (request.signal.aborted || sequence !== this._requestSequence) return;
@@ -20092,7 +20716,7 @@ var Tooltip = class extends Component {
20092
20716
  this.#content = this.options.content;
20093
20717
  this.#disabled = Boolean(this.options.disabled);
20094
20718
  this.#describedBy = resolved.getAttribute("aria-describedby");
20095
- this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength2(this.options.maxWidth));
20719
+ this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength3(this.options.maxWidth));
20096
20720
  const trigger = TRIGGERS.has(this.options.trigger) ? this.options.trigger : "both";
20097
20721
  if (trigger === "manual") return;
20098
20722
  if (trigger === "hover" || trigger === "both") {
@@ -20350,7 +20974,7 @@ function appendToken(list, token) {
20350
20974
  if (!tokens.includes(token)) tokens.push(token);
20351
20975
  return tokens.join(" ");
20352
20976
  }
20353
- function cssLength2(value) {
20977
+ function cssLength3(value) {
20354
20978
  return typeof value === "number" && Number.isFinite(value) ? `${value}px` : String(value);
20355
20979
  }
20356
20980
  function isFocusVisible(element) {
@@ -22851,7 +23475,7 @@ var Grid = class _Grid extends Table {
22851
23475
  * @param {GridOptions & import('../table/table.js').TableOptions} [options={}] Grid/Table options.
22852
23476
  */
22853
23477
  constructor(target = null, options = {}) {
22854
- super(target, options);
23478
+ super(target, { editTrigger: "single", ...options });
22855
23479
  }
22856
23480
  /**
22857
23481
  * Creates an editable hierarchical billing-line grid.
@@ -22890,6 +23514,9 @@ function billingItemsConfig(options = {}) {
22890
23514
  return {
22891
23515
  rowId: fields.id,
22892
23516
  editMode: "cell",
23517
+ editTrigger: "single",
23518
+ rowReorder: true,
23519
+ columnVisibility: true,
22893
23520
  hierarchy: { parentId: fields.parent, column: fields.item, expanded: true },
22894
23521
  responsive: "sm",
22895
23522
  ...tableOptions,
@@ -23121,8 +23748,8 @@ var Chart = class extends Component {
23121
23748
  return this;
23122
23749
  }
23123
23750
  /** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
23124
- setError(error) {
23125
- this._error = error ?? new Error(String(this.options.errorText));
23751
+ setError(error2) {
23752
+ this._error = error2 ?? new Error(String(this.options.errorText));
23126
23753
  this._hasError = true;
23127
23754
  this._loading = false;
23128
23755
  this._destroyHandle();
@@ -23141,8 +23768,8 @@ var Chart = class extends Component {
23141
23768
  resize() {
23142
23769
  try {
23143
23770
  this._handle?.resize?.();
23144
- } catch (error) {
23145
- this.setError(error);
23771
+ } catch (error2) {
23772
+ this.setError(error2);
23146
23773
  }
23147
23774
  return this;
23148
23775
  }
@@ -23195,8 +23822,8 @@ var Chart = class extends Component {
23195
23822
  this.emit("ready", { instance: this.getInstance() });
23196
23823
  }
23197
23824
  if (emitUpdate) this.emit("update", { data: this._data, type: this._type });
23198
- } catch (error) {
23199
- this.setError(error);
23825
+ } catch (error2) {
23826
+ this.setError(error2);
23200
23827
  }
23201
23828
  }
23202
23829
  /** @returns {ChartSpec} */
@@ -23438,8 +24065,8 @@ function themeChartOptions(root) {
23438
24065
  borderColor: borderColor || void 0
23439
24066
  };
23440
24067
  }
23441
- function errorMessage(error, fallback) {
23442
- const message = error && typeof error === "object" && "message" in error ? String(error.message) : "";
24068
+ function errorMessage(error2, fallback) {
24069
+ const message = error2 && typeof error2 === "object" && "message" in error2 ? String(error2.message) : "";
23443
24070
  return message.trim() ? message : String(fallback);
23444
24071
  }
23445
24072
 
@@ -24078,8 +24705,8 @@ var TreeView = class extends Component {
24078
24705
  node[this._childrenKey] = Array.isArray(children) ? children : [];
24079
24706
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
24080
24707
  return true;
24081
- } catch (error) {
24082
- this.emit("error", { error, node, id });
24708
+ } catch (error2) {
24709
+ this.emit("error", { error: error2, node, id });
24083
24710
  return false;
24084
24711
  } finally {
24085
24712
  this._loading.delete(id);
@@ -24567,9 +25194,9 @@ var Finder = class extends Component {
24567
25194
  const children = await this.options.load(node);
24568
25195
  node[this._childrenKey] = Array.isArray(children) ? children : [];
24569
25196
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
24570
- } catch (error) {
25197
+ } catch (error2) {
24571
25198
  node[this._childrenKey] = [];
24572
- this.emit("error", { error, node, id });
25199
+ this.emit("error", { error: error2, node, id });
24573
25200
  } finally {
24574
25201
  this._loading.delete(id);
24575
25202
  }
@@ -24999,6 +25626,821 @@ function restoreCustomElements(origins) {
24999
25626
  }
25000
25627
  }
25001
25628
 
25629
+ // src/components/filter/filter-model.js
25630
+ var filterOperators = Object.freeze([
25631
+ op("contains", "contains", "single", ["text"]),
25632
+ op("notContains", "does not contain", "single", ["text"]),
25633
+ op("eq", "is", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25634
+ op("neq", "is not", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25635
+ op("startsWith", "starts with", "single", ["text"]),
25636
+ op("endsWith", "ends with", "single", ["text"]),
25637
+ op("lt", "is less than", "single", ["number", "money"]),
25638
+ op("lte", "is at most", "single", ["number", "money"]),
25639
+ op("gt", "is greater than", "single", ["number", "money"]),
25640
+ op("gte", "is at least", "single", ["number", "money"]),
25641
+ op("on", "is on", "single", ["date", "datetime"]),
25642
+ op("notOn", "is not on", "single", ["date", "datetime"]),
25643
+ op("before", "is before", "single", ["date", "datetime"]),
25644
+ op("onOrBefore", "is on or before", "single", ["date", "datetime"]),
25645
+ op("after", "is after", "single", ["date", "datetime"]),
25646
+ op("onOrAfter", "is on or after", "single", ["date", "datetime"]),
25647
+ op("between", "is between", "pair", ["number", "money", "date", "datetime"]),
25648
+ op("notBetween", "is not between", "pair", ["number", "money", "date", "datetime"]),
25649
+ op("withinLast", "is within the last", "pair", ["date", "datetime"]),
25650
+ op("withinNext", "is within the next", "pair", ["date", "datetime"]),
25651
+ op("anyOf", "is any of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25652
+ op("noneOf", "is none of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25653
+ op("containsAny", "contains any", "many", ["tags"]),
25654
+ op("containsAll", "contains all", "many", ["tags"]),
25655
+ op("containsNone", "contains none", "many", ["tags"]),
25656
+ op("isEmpty", "is empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"]),
25657
+ op("isNotEmpty", "is not empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"])
25658
+ ]);
25659
+ function emptyFilterAst() {
25660
+ return { version: 1, root: { kind: "group", id: uid("filter-root"), logic: "and", children: [] } };
25661
+ }
25662
+ function filterCondition(initial = {}) {
25663
+ return {
25664
+ kind: "condition",
25665
+ id: String(initial.id || uid("filter-condition")),
25666
+ field: String(initial.field ?? ""),
25667
+ operator: String(initial.operator ?? ""),
25668
+ value: cloneFilterValue(initial.value ?? null)
25669
+ };
25670
+ }
25671
+ function cloneFilterValue(value) {
25672
+ return cloneJson(value);
25673
+ }
25674
+ function filterGroup(logic = "and", children = []) {
25675
+ return { kind: "group", id: uid("filter-group"), logic, children };
25676
+ }
25677
+ function parseFilterAst(value, limits = {}) {
25678
+ const maxDepth = finiteLimit(limits.maxDepth, 3);
25679
+ const maxConditions = finiteLimit(limits.maxConditions, 50);
25680
+ if (!plainObject(value) || value.version !== 1 || !plainObject(value.root)) {
25681
+ throw new TypeError("Filter AST must have version 1 and a root group");
25682
+ }
25683
+ const ids = /* @__PURE__ */ new Set();
25684
+ let conditions = 0;
25685
+ const visit = (raw2, depth, root2 = false) => {
25686
+ if (!plainObject(raw2)) throw new TypeError("Filter nodes must be plain objects");
25687
+ const id = String(raw2.id ?? "").trim();
25688
+ if (!id || ids.has(id)) throw new TypeError("Filter node IDs must be non-empty and unique");
25689
+ ids.add(id);
25690
+ if (raw2.kind === "group") {
25691
+ if (depth > maxDepth) throw new RangeError("Filter group depth exceeds maxDepth");
25692
+ if (raw2.logic !== "and" && raw2.logic !== "or") throw new TypeError("Filter group logic must be and or or");
25693
+ if (!Array.isArray(raw2.children)) throw new TypeError("Filter group children must be an array");
25694
+ return {
25695
+ kind: "group",
25696
+ id,
25697
+ logic: raw2.logic,
25698
+ children: raw2.children.map((child) => visit(child, depth + 1, false))
25699
+ };
25700
+ }
25701
+ if (root2 || raw2.kind !== "condition") throw new TypeError("Filter root must be a group");
25702
+ conditions += 1;
25703
+ if (conditions > maxConditions) throw new RangeError("Filter condition count exceeds maxConditions");
25704
+ return {
25705
+ kind: "condition",
25706
+ id,
25707
+ field: String(raw2.field ?? ""),
25708
+ operator: String(raw2.operator ?? ""),
25709
+ value: cloneJson(raw2.value)
25710
+ };
25711
+ };
25712
+ const root = visit(value.root, 0, true);
25713
+ if (root.kind !== "group") throw new TypeError("Filter root must be a group");
25714
+ return (
25715
+ /** @type {FilterAst} */
25716
+ { version: 1, root }
25717
+ );
25718
+ }
25719
+ function cloneFilterAst(value) {
25720
+ return parseFilterAst(value, { maxDepth: Infinity, maxConditions: Infinity });
25721
+ }
25722
+ function stringifyFilterAst(value) {
25723
+ return JSON.stringify(cloneFilterAst(value));
25724
+ }
25725
+ function validateFilterAst(ast, fields, operators = filterOperators) {
25726
+ const errors = [];
25727
+ const fieldMap = new Map((Array.isArray(fields) ? fields : []).map((field) => [String(field.id), field]));
25728
+ const operatorMap = new Map((Array.isArray(operators) ? operators : []).map((operator) => [String(operator.id), operator]));
25729
+ const visit = (node, root = false) => {
25730
+ if (node.kind === "group") {
25731
+ if (!root && node.children.length === 0) errors.push(error(node.id, "empty-group", "Nested groups need at least one condition"));
25732
+ node.children.forEach((child) => visit(child));
25733
+ return;
25734
+ }
25735
+ const field = fieldMap.get(node.field);
25736
+ if (!field) errors.push(error(node.id, "unknown-field", node.field ? `Unavailable field: ${node.field}` : "Choose a field"));
25737
+ const operator = operatorMap.get(node.operator);
25738
+ if (!operator) {
25739
+ errors.push(error(node.id, "unknown-operator", node.operator ? `Unavailable operator: ${node.operator}` : "Choose an operator"));
25740
+ return;
25741
+ }
25742
+ if (field) {
25743
+ const allowed = Array.isArray(field.operators) && field.operators.length ? field.operators : operators.filter((candidate) => candidate.types.includes(field.type)).map((candidate) => candidate.id);
25744
+ if (!allowed.includes(operator.id) || !operator.types.includes(field.type)) {
25745
+ errors.push(error(node.id, "operator-type", `${operator.label} is not available for ${field.label}`));
25746
+ }
25747
+ }
25748
+ if (!validArity(operator.arity, node.value)) {
25749
+ errors.push(error(node.id, "value", operator.arity === "pair" ? "Enter both values" : operator.arity === "many" ? "Choose at least one value" : operator.arity === "none" ? "This operator does not accept a value" : "Enter a value"));
25750
+ }
25751
+ };
25752
+ visit(ast.root, true);
25753
+ return { valid: errors.length === 0, errors };
25754
+ }
25755
+ function op(id, label, arity, types) {
25756
+ return Object.freeze({ id, label, arity, types: Object.freeze(types) });
25757
+ }
25758
+ function error(nodeId, code, message) {
25759
+ return { nodeId, code, message };
25760
+ }
25761
+ function validArity(arity, value) {
25762
+ if (arity === "none") return value === null;
25763
+ if (arity === "pair") return Array.isArray(value) && value.length === 2 && value.every(filled);
25764
+ if (arity === "many") return Array.isArray(value) && value.length > 0 && value.every(filled);
25765
+ return filled(value);
25766
+ }
25767
+ function filled(value) {
25768
+ return value !== null && value !== void 0 && !(typeof value === "string" && value.trim() === "");
25769
+ }
25770
+ function cloneJson(value, seen = /* @__PURE__ */ new WeakSet()) {
25771
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
25772
+ if (typeof value === "number") {
25773
+ if (!Number.isFinite(value)) throw new TypeError("Filter values cannot contain non-finite numbers");
25774
+ return value;
25775
+ }
25776
+ if (typeof value !== "object" || value instanceof Date || typeof value.nodeType === "number") {
25777
+ throw new TypeError("Filter values must be JSON-safe");
25778
+ }
25779
+ if (seen.has(value)) throw new TypeError("Filter values cannot contain cycles");
25780
+ seen.add(value);
25781
+ if (Array.isArray(value)) {
25782
+ const result2 = value.map((item) => cloneJson(item, seen));
25783
+ seen.delete(value);
25784
+ return result2;
25785
+ }
25786
+ if (!plainObject(value)) throw new TypeError("Filter values must be plain JSON objects");
25787
+ const result = {};
25788
+ for (const [key, item] of Object.entries(value)) {
25789
+ Object.defineProperty(result, key, {
25790
+ value: cloneJson(item, seen),
25791
+ enumerable: true,
25792
+ configurable: true,
25793
+ writable: true
25794
+ });
25795
+ }
25796
+ seen.delete(value);
25797
+ return result;
25798
+ }
25799
+ function plainObject(value) {
25800
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
25801
+ const prototype = Object.getPrototypeOf(value);
25802
+ return prototype === Object.prototype || prototype === null;
25803
+ }
25804
+ function finiteLimit(value, fallback) {
25805
+ if (value === Infinity) return Infinity;
25806
+ const number2 = Number(value);
25807
+ return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : fallback;
25808
+ }
25809
+
25810
+ // src/components/filter/filter.js
25811
+ var Filter = class extends Component {
25812
+ static cssName = "filter";
25813
+ /** @type {Readonly<FilterOptions>} */
25814
+ static defaults = {
25815
+ fields: [],
25816
+ value: null,
25817
+ operators: [],
25818
+ allowGroups: true,
25819
+ maxDepth: 3,
25820
+ maxConditions: 50,
25821
+ autoApply: false,
25822
+ readonly: false,
25823
+ disabled: false,
25824
+ applyLabel: "Apply filters",
25825
+ clearLabel: "Clear"
25826
+ };
25827
+ /** @returns {HTMLElement} */
25828
+ render() {
25829
+ this._createdRoot = this.el === null;
25830
+ const root = (
25831
+ /** @type {HTMLElement} */
25832
+ this.el ?? h("div")
25833
+ );
25834
+ this.el = root;
25835
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
25836
+ this._fields = (Array.isArray(this.options.fields) ? this.options.fields : []).map((field) => ({ ...field }));
25837
+ this._operators = mergeOperators(filterOperators, this.options.operators);
25838
+ this._readonly = Boolean(this.options.readonly);
25839
+ this._disabled = Boolean(this.options.disabled);
25840
+ this._async = /* @__PURE__ */ new Map();
25841
+ this._customEditors = [];
25842
+ this._value = this.options.value == null ? emptyFilterAst() : parseFilterAst(this.options.value, this._limits());
25843
+ this.listen(root, "change", (event) => this._handleChange(event));
25844
+ this.listen(root, "input", (event) => this._handleInput(event));
25845
+ this.listen(root, "click", (event) => this._handleClick(event));
25846
+ this._render();
25847
+ return root;
25848
+ }
25849
+ /** Returns a defensive AST copy. @returns {FilterAst} */
25850
+ getValue() {
25851
+ return cloneFilterAst(this._value);
25852
+ }
25853
+ /** Atomically replaces the AST. @param {unknown} value @param {{silent?:boolean}} [options={}] @returns {this} */
25854
+ setValue(value, { silent = false } = {}) {
25855
+ const parsed = parseFilterAst(value, this._limits());
25856
+ this._abortAll();
25857
+ this._value = parsed;
25858
+ this._render();
25859
+ if (!silent) this._changed("set", this._value.root.id);
25860
+ return this;
25861
+ }
25862
+ /** Adds a condition to a group. @param {string|null} [parentId=null] @param {Record<string,unknown>} [initial={}] @returns {string|null} */
25863
+ addCondition(parentId = null, initial = {}) {
25864
+ if (!this._canMutate() || conditionCount(this._value.root) >= Number(this.options.maxConditions)) return null;
25865
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
25866
+ if (!parent) return null;
25867
+ const first = this._fields[0];
25868
+ const field = String(initial.field ?? first?.id ?? "");
25869
+ const definition = this._field(field);
25870
+ const node = filterCondition({
25871
+ ...initial,
25872
+ field,
25873
+ operator: String(initial.operator ?? this._defaultOperator(definition) ?? "")
25874
+ });
25875
+ parent.children.push(node);
25876
+ this._render();
25877
+ this._changed("add", node.id);
25878
+ this.focus(node.id);
25879
+ return node.id;
25880
+ }
25881
+ /** Adds a nested group. @param {string|null} [parentId=null] @param {'and'|'or'} [logic='and'] @returns {string|null} */
25882
+ addGroup(parentId = null, logic = "and") {
25883
+ if (!this._canMutate() || !this.options.allowGroups) return null;
25884
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
25885
+ if (!parent || groupDepth(this._value.root, parent.id) >= Number(this.options.maxDepth)) return null;
25886
+ const group = filterGroup(logic === "or" ? "or" : "and");
25887
+ parent.children.push(group);
25888
+ this._render();
25889
+ this._changed("add", group.id);
25890
+ this.focus(group.id);
25891
+ return group.id;
25892
+ }
25893
+ /** Updates a node by stable ID. @param {string} id @param {Record<string,unknown>} patch @returns {this} */
25894
+ update(id, patch = {}) {
25895
+ if (!this._canMutate()) return this;
25896
+ const node = findNode2(this._value.root, id);
25897
+ if (!node) return this;
25898
+ if (node.kind === "group") {
25899
+ if (patch.logic === "and" || patch.logic === "or") node.logic = patch.logic;
25900
+ } else {
25901
+ if (Object.hasOwn(patch, "field")) node.field = String(patch.field ?? "");
25902
+ if (Object.hasOwn(patch, "operator")) node.operator = String(patch.operator ?? "");
25903
+ if (Object.hasOwn(patch, "value")) node.value = parseValueJson(patch.value);
25904
+ }
25905
+ this._abort(id);
25906
+ this._render();
25907
+ this._changed("update", id);
25908
+ return this;
25909
+ }
25910
+ /** Removes a non-root node. @param {string} id @returns {this} */
25911
+ remove(id) {
25912
+ if (!this._canMutate() || id === this._value.root.id) return this;
25913
+ const parent = findParent(this._value.root, id);
25914
+ if (!parent) return this;
25915
+ const index = parent.children.findIndex((child) => child.id === id);
25916
+ const focusId = parent.children[index + 1]?.id ?? parent.children[index - 1]?.id ?? parent.id;
25917
+ const removed = removeNode(this._value.root, id);
25918
+ if (!removed) return this;
25919
+ this._abortTree(removed);
25920
+ this._render();
25921
+ const label = removed.kind === "condition" ? this._field(removed.field)?.label ?? "Filter" : "Filter group";
25922
+ this._announce(`${label} removed`);
25923
+ this._changed("remove", id);
25924
+ this.focus(focusId);
25925
+ return this;
25926
+ }
25927
+ /** Moves a node into a group while preserving its identity. @param {string} id @param {string} parentId @param {number} index @returns {this} */
25928
+ move(id, parentId, index) {
25929
+ const node = findNode2(this._value.root, id);
25930
+ const parent = findGroup(this._value.root, parentId);
25931
+ const previous = findParent(this._value.root, id);
25932
+ if (!this._canMutate() || id === this._value.root.id || !node || !parent || !previous || containsNode(node, parentId)) return this;
25933
+ const previousIndex = previous.children.findIndex((child) => child.id === id);
25934
+ const removed = previous.children.splice(previousIndex, 1)[0];
25935
+ parent.children.splice(Math.max(0, Math.min(parent.children.length, Number(index) || 0)), 0, removed);
25936
+ try {
25937
+ parseFilterAst(this._value, this._limits());
25938
+ } catch {
25939
+ parent.children.splice(parent.children.indexOf(removed), 1);
25940
+ previous.children.splice(previousIndex, 0, removed);
25941
+ return this;
25942
+ }
25943
+ this._render();
25944
+ this._changed("move", id);
25945
+ return this;
25946
+ }
25947
+ /** Returns semantic validity and errors. @returns {{valid:boolean,errors:Array<Record<string,string>>}} */
25948
+ validate() {
25949
+ return validateFilterAst(this._value, this._fields, this._operators);
25950
+ }
25951
+ /** Applies only a valid AST. @returns {FilterAst|null} */
25952
+ apply() {
25953
+ if (this._disabled) return null;
25954
+ const result = this.validate();
25955
+ if (!result.valid) {
25956
+ this.emit("invalid", { errors: result.errors });
25957
+ this.focus(result.errors[0]?.nodeId);
25958
+ return null;
25959
+ }
25960
+ const value = this.getValue();
25961
+ this.emit("apply", { value });
25962
+ return value;
25963
+ }
25964
+ /** Clears all conditions. @param {{silent?:boolean}} [options={}] @returns {this} */
25965
+ clear({ silent = false } = {}) {
25966
+ if (!this._canMutate()) return this;
25967
+ this._abortAll();
25968
+ this._value.root.children = [];
25969
+ this._render();
25970
+ if (!silent) this._changed("clear", this._value.root.id);
25971
+ this.refs.add?.focus();
25972
+ return this;
25973
+ }
25974
+ /** Focuses a condition's field or the Add button. @param {string|null} [id=null] @returns {this} */
25975
+ focus(id = null) {
25976
+ const selector = id ? `[data-node-id="${cssEscape(id)}"] :is([data-filter-field], [data-filter-logic], [data-filter-action="add"])` : '[data-filter-action="add"]';
25977
+ queueMicrotask(() => (
25978
+ /** @type {HTMLElement|null} */
25979
+ this.el.querySelector(selector)?.focus()
25980
+ ));
25981
+ return this;
25982
+ }
25983
+ /** @param {boolean} readonly @returns {this} */
25984
+ setReadonly(readonly) {
25985
+ this._readonly = Boolean(readonly);
25986
+ this._render();
25987
+ return this;
25988
+ }
25989
+ /** @returns {this} */
25990
+ enable() {
25991
+ this._disabled = false;
25992
+ this._render();
25993
+ return this;
25994
+ }
25995
+ /** @returns {this} */
25996
+ disable() {
25997
+ this._disabled = true;
25998
+ this._abortAll();
25999
+ this._render();
26000
+ return this;
26001
+ }
26002
+ /** @returns {void} */
26003
+ destroy() {
26004
+ this._abortAll();
26005
+ this._destroyEditors();
26006
+ const root = this.el;
26007
+ const snapshot10 = this._snapshot;
26008
+ super.destroy();
26009
+ if (!this._createdRoot && root) restoreTarget(root, snapshot10);
26010
+ }
26011
+ /** @returns {void} */
26012
+ _render() {
26013
+ this._destroyEditors();
26014
+ this._renderVersion = (this._renderVersion ?? 0) + 1;
26015
+ const status = h("div", { ref: "status", class: "zx-filter__status", role: "status", ariaLive: "polite" });
26016
+ const tree = this._renderGroup(this._value.root, 0, true);
26017
+ const actions2 = h(
26018
+ "div",
26019
+ { class: "zx-filter__actions" },
26020
+ actionButton("add", this._value.root.id, "plus", "Add filter", { ref: "add", disabled: this._blocked() }),
26021
+ this.options.allowGroups ? actionButton("add-group", this._value.root.id, "folder", "Add group", { disabled: this._blocked() }) : null,
26022
+ actionButton("clear", "", "x", String(this.options.clearLabel), { disabled: this._blocked() }),
26023
+ actionButton("apply", "", "check", String(this.options.applyLabel), { class: "zx-button--primary", disabled: this._disabled })
26024
+ );
26025
+ this.el.replaceChildren(status, tree, actions2);
26026
+ this.refs.status = status;
26027
+ this.refs.add = /** @type {HTMLElement} */
26028
+ this.el.querySelector('[data-filter-action="add"]');
26029
+ this.el.toggleAttribute("data-readonly", this._readonly);
26030
+ this.el.toggleAttribute("data-disabled", this._disabled);
26031
+ const result = this.validate();
26032
+ this.el.dataset.valid = String(result.valid);
26033
+ for (const error2 of result.errors) this.el.querySelector(`[data-node-id="${cssEscape(error2.nodeId)}"]`)?.setAttribute("data-invalid", "");
26034
+ }
26035
+ /** @param {any} group @param {number} depth @param {boolean} root @returns {HTMLElement} */
26036
+ _renderGroup(group, depth, root) {
26037
+ const children = h(
26038
+ "ol",
26039
+ { class: "zx-filter__children" },
26040
+ group.children.map((node) => node.kind === "group" ? this._renderGroup(node, depth + 1, false) : this._renderCondition(node))
26041
+ );
26042
+ const logic = h(
26043
+ "select",
26044
+ {
26045
+ class: "zx-filter__logic",
26046
+ dataset: { filterLogic: "", nodeId: group.id },
26047
+ ariaLabel: root ? "Root filter logic" : "Group filter logic",
26048
+ disabled: this._blocked()
26049
+ },
26050
+ h("option", { value: "and", selected: group.logic === "and" }, "Match all"),
26051
+ h("option", { value: "or", selected: group.logic === "or" }, "Match any")
26052
+ );
26053
+ const legend = h("legend", { class: "zx-filter__legend" }, logic, h("span", {}, root ? "conditions" : "in this group"));
26054
+ const controls = h(
26055
+ "div",
26056
+ { class: "zx-filter__group-actions" },
26057
+ actionButton("add", group.id, "plus", "Add filter", { disabled: this._blocked() }),
26058
+ this.options.allowGroups && depth < Number(this.options.maxDepth) ? actionButton("add-group", group.id, "folder", "Add subgroup", { disabled: this._blocked() }) : null,
26059
+ root ? null : actionButton("remove", group.id, "trash", "Remove group", { disabled: this._blocked() })
26060
+ );
26061
+ return h(root ? "fieldset" : "li", {
26062
+ class: "zx-filter__group",
26063
+ dataset: { nodeId: group.id, depth: String(depth) }
26064
+ }, root ? legend : h("fieldset", {}, legend, children, controls), root ? children : null, root ? controls : null);
26065
+ }
26066
+ /** @param {any} node @returns {HTMLElement} */
26067
+ _renderCondition(node) {
26068
+ const field = this._field(node.field);
26069
+ const operators = this._operatorsFor(field);
26070
+ const operator = this._operator(node.operator);
26071
+ const fieldSelect = h(
26072
+ "select",
26073
+ {
26074
+ class: "zx-filter__field",
26075
+ dataset: { filterField: "", nodeId: node.id },
26076
+ ariaLabel: "Filter field",
26077
+ disabled: this._blocked()
26078
+ },
26079
+ h("option", { value: "", selected: !node.field }, "Choose a field\u2026"),
26080
+ this._fields.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.field }, candidate.label)),
26081
+ node.field && !field ? h("option", { value: node.field, selected: true }, `Unavailable: ${node.field}`) : null
26082
+ );
26083
+ const operatorSelect = h(
26084
+ "select",
26085
+ {
26086
+ class: "zx-filter__operator",
26087
+ dataset: { filterOperator: "", nodeId: node.id },
26088
+ ariaLabel: "Filter operator",
26089
+ disabled: this._blocked()
26090
+ },
26091
+ h("option", { value: "", selected: !node.operator }, "Choose an operator\u2026"),
26092
+ operators.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.operator }, candidate.label)),
26093
+ node.operator && !operators.some((candidate) => candidate.id === node.operator) ? h("option", { value: node.operator, selected: true }, `Unavailable: ${node.operator}`) : null
26094
+ );
26095
+ return h(
26096
+ "li",
26097
+ { class: "zx-filter__condition", dataset: { nodeId: node.id } },
26098
+ fieldSelect,
26099
+ operatorSelect,
26100
+ this._renderValue(node, field, operator),
26101
+ actionButton("remove", node.id, "trash", `Remove ${field?.label ?? "filter"}`, { disabled: this._blocked() })
26102
+ );
26103
+ }
26104
+ /** @param {any} node @param {FilterField|null} field @param {any} operator @returns {HTMLElement} */
26105
+ _renderValue(node, field, operator) {
26106
+ const host = h("div", { class: "zx-filter__value" });
26107
+ if (!field || !operator) return host;
26108
+ if (operator.arity === "none") return h("div", { class: "zx-filter__value zx-filter__value--empty" }, "No value");
26109
+ if (field.type === "custom" && typeof field.editor === "function") {
26110
+ const renderVersion = this._renderVersion;
26111
+ queueMicrotask(() => {
26112
+ if (renderVersion !== this._renderVersion || !this.el.contains(host)) return;
26113
+ const handle = field.editor(host, {
26114
+ value: node.value,
26115
+ field,
26116
+ conditionId: node.id,
26117
+ setValue: (value) => this._setNodeValue(node.id, parseValueJson(value))
26118
+ });
26119
+ if (handle && typeof handle.destroy === "function") this._customEditors.push(handle);
26120
+ });
26121
+ return host;
26122
+ }
26123
+ const values = operator.arity === "pair" ? Array.isArray(node.value) ? node.value : [null, null] : [node.value];
26124
+ if (operator.arity === "many") {
26125
+ const control = choiceSelect(field, node, values[0], true, this._blocked());
26126
+ if (control) return h("div", { class: "zx-filter__value" }, control);
26127
+ return h("div", { class: "zx-filter__value" }, valueInput(field, node.id, Array.isArray(node.value) ? node.value.join(", ") : "", 0, this._blocked(), true));
26128
+ }
26129
+ const controls = values.map((value, index) => choiceSelect(field, node, value, false, this._blocked(), index) ?? valueInput(field, node.id, value, index, this._blocked()));
26130
+ return h("div", { class: "zx-filter__value", dataset: { arity: operator.arity } }, controls);
26131
+ }
26132
+ /** @param {Event} event @returns {void} */
26133
+ _handleChange(event) {
26134
+ if (!this._canMutate()) return;
26135
+ const target = (
26136
+ /** @type {HTMLInputElement|HTMLSelectElement|null} */
26137
+ event.target
26138
+ );
26139
+ const id = target?.dataset.nodeId;
26140
+ if (!target || !id) return;
26141
+ if (target.dataset.filterField !== void 0) {
26142
+ const node = findNode2(this._value.root, id);
26143
+ if (node?.kind !== "condition") return;
26144
+ node.field = target.value;
26145
+ const field = this._field(node.field);
26146
+ node.operator = this._defaultOperator(field) ?? "";
26147
+ node.value = null;
26148
+ this._abort(id);
26149
+ this._render();
26150
+ this._changed("update", id);
26151
+ this.focus(id);
26152
+ } else if (target.dataset.filterOperator !== void 0) {
26153
+ const node = findNode2(this._value.root, id);
26154
+ if (node?.kind !== "condition") return;
26155
+ node.operator = target.value;
26156
+ node.value = null;
26157
+ this._abort(id);
26158
+ this._render();
26159
+ this._changed("update", id);
26160
+ queueMicrotask(() => (
26161
+ /** @type {HTMLElement|null} */
26162
+ this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`)?.focus()
26163
+ ));
26164
+ } else if (target.dataset.filterLogic !== void 0) {
26165
+ const node = findNode2(this._value.root, id);
26166
+ if (node?.kind === "group") node.logic = target.value === "or" ? "or" : "and";
26167
+ this._changed("update", id);
26168
+ } else if (target.dataset.filterValue !== void 0) this._valueInput(target);
26169
+ }
26170
+ /** @param {Event} event @returns {void} */
26171
+ _handleInput(event) {
26172
+ if (!this._canMutate()) return;
26173
+ const target = (
26174
+ /** @type {HTMLInputElement|null} */
26175
+ event.target
26176
+ );
26177
+ if (!target || target.dataset.filterValue === void 0) return;
26178
+ this._valueInput(target);
26179
+ const id = target.dataset.nodeId;
26180
+ const node = findNode2(this._value.root, id);
26181
+ const field = node?.kind === "condition" ? this._field(node.field) : null;
26182
+ if (field?.loadChoices) this._scheduleChoices(node.id, field, target.value);
26183
+ }
26184
+ /** @param {HTMLInputElement|HTMLSelectElement} target @returns {void} */
26185
+ _valueInput(target) {
26186
+ const node = findNode2(this._value.root, target.dataset.nodeId);
26187
+ if (node?.kind !== "condition") return;
26188
+ const operator = this._operator(node.operator);
26189
+ const field = this._field(node.field);
26190
+ if (!operator || !field) return;
26191
+ let value;
26192
+ if (target instanceof HTMLSelectElement && target.multiple) {
26193
+ value = Array.from(target.selectedOptions, (option) => parseScalar(option.value, field.type));
26194
+ } else if (operator.arity === "many") {
26195
+ value = target.value.split(",").map((item) => item.trim()).filter(Boolean).map((item) => parseScalar(item, field.type));
26196
+ } else if (operator.arity === "pair") {
26197
+ const pair = Array.isArray(node.value) ? [...node.value] : [null, null];
26198
+ pair[Number(target.dataset.valueIndex) || 0] = parseScalar(target.value, field.type);
26199
+ value = pair;
26200
+ } else value = parseScalar(target.value, field.type);
26201
+ this._setNodeValue(
26202
+ node.id,
26203
+ /** @type {any} */
26204
+ value
26205
+ );
26206
+ }
26207
+ /** @param {MouseEvent} event @returns {void} */
26208
+ _handleClick(event) {
26209
+ const button2 = (
26210
+ /** @type {HTMLElement|null} */
26211
+ event.target?.closest?.("[data-filter-action]")
26212
+ );
26213
+ if (!button2 || !this.el.contains(button2)) return;
26214
+ const action = button2.dataset.filterAction;
26215
+ const id = button2.dataset.nodeId || null;
26216
+ if (action === "add") this.addCondition(id);
26217
+ else if (action === "add-group") this.addGroup(id);
26218
+ else if (action === "remove" && id) this.remove(id);
26219
+ else if (action === "clear") this.clear();
26220
+ else if (action === "apply") this.apply();
26221
+ }
26222
+ /** @param {string} id @param {any} value @returns {void} */
26223
+ _setNodeValue(id, value) {
26224
+ const node = findNode2(this._value.root, id);
26225
+ if (node?.kind !== "condition") return;
26226
+ node.value = value;
26227
+ const result = this.validate();
26228
+ this.el.dataset.valid = String(result.valid);
26229
+ const row2 = this.el.querySelector(`[data-node-id="${cssEscape(id)}"]`);
26230
+ row2?.toggleAttribute("data-invalid", result.errors.some((candidate) => candidate.nodeId === id));
26231
+ this._changed("update", id);
26232
+ }
26233
+ /** @param {string} reason @param {string} nodeId @returns {void} */
26234
+ _changed(reason, nodeId) {
26235
+ const result = this.validate();
26236
+ const value = this.getValue();
26237
+ this.emit("change", { value, valid: result.valid, errors: result.errors, reason, nodeId });
26238
+ if (this.options.autoApply && result.valid) this.emit("apply", { value });
26239
+ }
26240
+ /** @param {string} id @param {FilterField} field @param {string} query @returns {void} */
26241
+ _scheduleChoices(id, field, query) {
26242
+ this._abort(id);
26243
+ if (query.length < Number(field.minQuery ?? 0)) return;
26244
+ const controller = new AbortController();
26245
+ this._setAsyncBusy(id, true);
26246
+ const entry = { controller, timer: setTimeout(async () => {
26247
+ this.emit("query", { conditionId: id, fieldId: field.id, query });
26248
+ try {
26249
+ const choices = await field.loadChoices({ query, signal: controller.signal, conditionId: id, field });
26250
+ if (controller.signal.aborted || this._async.get(id)?.controller !== controller) return;
26251
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26252
+ const listId = input?.getAttribute("list");
26253
+ const list = listId ? this.el.querySelector(`#${cssEscape(listId)}`) : null;
26254
+ if (list) list.replaceChildren(...(Array.isArray(choices) ? choices : []).map((choice) => h("option", { value: choice.value }, choice.label)));
26255
+ this.emit("loaded", { conditionId: id, fieldId: field.id, query, choices });
26256
+ } catch (error2) {
26257
+ if (!controller.signal.aborted) this.emit("error", { conditionId: id, fieldId: field.id, query, error: error2 instanceof Error ? error2 : new Error(String(error2)) });
26258
+ } finally {
26259
+ if (this._async.get(id)?.controller === controller) {
26260
+ this._async.delete(id);
26261
+ this._setAsyncBusy(id, false);
26262
+ }
26263
+ }
26264
+ }, Math.max(0, Number(field.debounce ?? 200))) };
26265
+ this._async.set(id, entry);
26266
+ }
26267
+ /** @param {string} id @returns {void} */
26268
+ _abort(id) {
26269
+ const entry = this._async.get(id);
26270
+ if (entry) {
26271
+ clearTimeout(entry.timer);
26272
+ entry.controller.abort();
26273
+ this._async.delete(id);
26274
+ this._setAsyncBusy(id, false);
26275
+ }
26276
+ }
26277
+ /** @param {string} id @param {boolean} busy @returns {void} */
26278
+ _setAsyncBusy(id, busy) {
26279
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26280
+ if (busy) input?.setAttribute("aria-busy", "true");
26281
+ else input?.removeAttribute("aria-busy");
26282
+ }
26283
+ /** @param {any} node @returns {void} */
26284
+ _abortTree(node) {
26285
+ if (node.kind === "condition") this._abort(node.id);
26286
+ else node.children.forEach((child) => this._abortTree(child));
26287
+ }
26288
+ /** @returns {void} */
26289
+ _abortAll() {
26290
+ for (const id of [...this._async.keys()]) this._abort(id);
26291
+ }
26292
+ /** @returns {void} */
26293
+ _destroyEditors() {
26294
+ for (const editor of this._customEditors ?? []) editor.destroy?.();
26295
+ this._customEditors = [];
26296
+ }
26297
+ /** @returns {boolean} */
26298
+ _blocked() {
26299
+ return this._readonly || this._disabled;
26300
+ }
26301
+ /** @returns {boolean} */
26302
+ _canMutate() {
26303
+ return !this._blocked();
26304
+ }
26305
+ /** @returns {{maxDepth:number,maxConditions:number}} */
26306
+ _limits() {
26307
+ return { maxDepth: Number(this.options.maxDepth), maxConditions: Number(this.options.maxConditions) };
26308
+ }
26309
+ /** @param {string} id @returns {FilterField|null} */
26310
+ _field(id) {
26311
+ return this._fields.find((field) => field.id === id) ?? null;
26312
+ }
26313
+ /** @param {string} id @returns {any} */
26314
+ _operator(id) {
26315
+ return this._operators.find((operator) => operator.id === id) ?? null;
26316
+ }
26317
+ /** @param {FilterField|null} field @returns {any[]} */
26318
+ _operatorsFor(field) {
26319
+ if (!field) return [];
26320
+ const ids = field.operators?.length ? field.operators : null;
26321
+ return this._operators.filter((operator) => ids ? ids.includes(operator.id) : operator.types.includes(field.type));
26322
+ }
26323
+ /** @param {FilterField|null} field @returns {string|null} */
26324
+ _defaultOperator(field) {
26325
+ return field?.defaultOperator && this._operatorsFor(field).some((operator) => operator.id === field.defaultOperator) ? field.defaultOperator : this._operatorsFor(field)[0]?.id ?? null;
26326
+ }
26327
+ /** @param {string} text @returns {void} */
26328
+ _announce(text4) {
26329
+ if (this.refs.status) this.refs.status.textContent = text4;
26330
+ }
26331
+ };
26332
+ function actionButton(action, id, iconName, label, props = {}) {
26333
+ return h("button", { type: "button", class: ["zx-button", "zx-button--sm", props.class].filter(Boolean), ...props, dataset: { filterAction: action, nodeId: id } }, icon(iconName, { size: 13 }), h("span", {}, label));
26334
+ }
26335
+ function choiceSelect(field, node, value, multiple, disabled, index = 0) {
26336
+ const choices = Array.isArray(field.choices) && field.choices.length ? field.choices : field.type === "boolean" ? [{ value: true, label: "True" }, { value: false, label: "False" }] : null;
26337
+ if (!choices) return null;
26338
+ const values = new Set(Array.isArray(value) ? value.map(String) : [String(value ?? "")]);
26339
+ return (
26340
+ /** @type {HTMLSelectElement} */
26341
+ h(
26342
+ "select",
26343
+ {
26344
+ dataset: { filterValue: "", nodeId: node.id, valueIndex: String(index) },
26345
+ ariaLabel: `${field.label} value`,
26346
+ multiple,
26347
+ disabled
26348
+ },
26349
+ !multiple ? h("option", { value: "", selected: !values.has(String(value)) || value == null }, "Choose\u2026") : null,
26350
+ choices.map((choice) => h("option", { value: choice.value, selected: values.has(String(choice.value)) }, choice.label))
26351
+ )
26352
+ );
26353
+ }
26354
+ function valueInput(field, id, value, index, disabled, many = false) {
26355
+ const type = many ? "text" : ["number", "money"].includes(field.type) ? "number" : field.type === "date" ? "date" : field.type === "datetime" ? "datetime-local" : "text";
26356
+ const listId = field.loadChoices ? `zx-filter-choices-${id}-${index}` : null;
26357
+ const input = (
26358
+ /** @type {HTMLInputElement} */
26359
+ h("input", {
26360
+ type,
26361
+ value: value ?? "",
26362
+ dataset: { filterValue: "", nodeId: id, valueIndex: String(index) },
26363
+ ariaLabel: `${field.label} value${index ? ` ${index + 1}` : ""}`,
26364
+ disabled,
26365
+ placeholder: many ? "Comma-separated values" : "",
26366
+ list: listId
26367
+ })
26368
+ );
26369
+ return h(
26370
+ "span",
26371
+ { class: "zx-filter__value-control" },
26372
+ input,
26373
+ listId ? h("datalist", { id: listId }) : null
26374
+ );
26375
+ }
26376
+ function parseScalar(value, type) {
26377
+ if (value === "") return null;
26378
+ if (["number", "money"].includes(type)) {
26379
+ const number2 = Number(value);
26380
+ return Number.isFinite(number2) ? number2 : null;
26381
+ }
26382
+ if (type === "boolean") return value === "true";
26383
+ return value;
26384
+ }
26385
+ function parseValueJson(value) {
26386
+ return cloneFilterValue(value === void 0 ? null : value);
26387
+ }
26388
+ function findNode2(root, id) {
26389
+ if (!root || root.id === id) return root ?? null;
26390
+ if (root.kind === "group") for (const child of root.children) {
26391
+ const match = findNode2(child, id);
26392
+ if (match) return match;
26393
+ }
26394
+ return null;
26395
+ }
26396
+ function findGroup(root, id) {
26397
+ const node = findNode2(root, id);
26398
+ return node?.kind === "group" ? node : null;
26399
+ }
26400
+ function findParent(root, id) {
26401
+ if (root?.kind !== "group") return null;
26402
+ if (root.children.some((child) => child.id === id)) return root;
26403
+ for (const child of root.children) {
26404
+ const parent = findParent(child, id);
26405
+ if (parent) return parent;
26406
+ }
26407
+ return null;
26408
+ }
26409
+ function removeNode(root, id) {
26410
+ if (root.kind !== "group") return null;
26411
+ const index = root.children.findIndex((child) => child.id === id);
26412
+ if (index >= 0) return root.children.splice(index, 1)[0];
26413
+ for (const child of root.children) {
26414
+ const found = removeNode(child, id);
26415
+ if (found) return found;
26416
+ }
26417
+ return null;
26418
+ }
26419
+ function containsNode(node, id) {
26420
+ return Boolean(node && findNode2(node, id));
26421
+ }
26422
+ function conditionCount(node) {
26423
+ return node.kind === "condition" ? 1 : node.children.reduce((sum, child) => sum + conditionCount(child), 0);
26424
+ }
26425
+ function groupDepth(root, id, depth = 0) {
26426
+ if (root.id === id) return depth;
26427
+ if (root.kind === "group") for (const child of root.children) {
26428
+ if (child.kind === "group") {
26429
+ const found = groupDepth(child, id, depth + 1);
26430
+ if (found >= 0) return found;
26431
+ }
26432
+ }
26433
+ return -1;
26434
+ }
26435
+ function mergeOperators(base, extra) {
26436
+ const map = new Map(base.map((item) => [item.id, { ...item, types: [...item.types] }]));
26437
+ for (const item of Array.isArray(extra) ? extra : []) if (item?.id) map.set(String(item.id), { ...item, id: String(item.id), types: Array.isArray(item.types) ? [...item.types] : [] });
26438
+ return [...map.values()];
26439
+ }
26440
+ function cssEscape(value) {
26441
+ return globalThis.CSS?.escape ? CSS.escape(value) : String(value).replaceAll(/[^a-zA-Z0-9_-]/g, "\\$&");
26442
+ }
26443
+
25002
26444
  // src/components/fieldset/fieldset.js
25003
26445
  var Fieldset = class extends Component {
25004
26446
  static cssName = "fieldset";
@@ -26166,8 +27608,8 @@ var Questionnaire = class extends Component {
26166
27608
  this._setBusy(true);
26167
27609
  try {
26168
27610
  message = await item.validate(answer, this.getAnswers());
26169
- } catch (error) {
26170
- message = error instanceof Error ? error.message : String(error);
27611
+ } catch (error2) {
27612
+ message = error2 instanceof Error ? error2.message : String(error2);
26171
27613
  } finally {
26172
27614
  if (!this._destroyed) this._setBusy(false);
26173
27615
  }
@@ -26305,10 +27747,10 @@ var Questionnaire = class extends Component {
26305
27747
  }
26306
27748
  /** @param {NormalizedItem} item @returns {void} */
26307
27749
  _paintError(item) {
26308
- const error = this._itemHost.querySelector(".zx-questionnaire__error");
26309
- if (error instanceof HTMLElement) {
26310
- error.textContent = this._error;
26311
- error.hidden = !this._error;
27750
+ const error2 = this._itemHost.querySelector(".zx-questionnaire__error");
27751
+ if (error2 instanceof HTMLElement) {
27752
+ error2.textContent = this._error;
27753
+ error2.hidden = !this._error;
26312
27754
  }
26313
27755
  if (this._error) this._itemHost.setAttribute("aria-invalid", "true");
26314
27756
  else this._itemHost.removeAttribute("aria-invalid");
@@ -26316,7 +27758,7 @@ var Questionnaire = class extends Component {
26316
27758
  if (field) {
26317
27759
  if (this._error) field.setHighlight(this._error, "danger");
26318
27760
  else field.clearHighlight();
26319
- if (error instanceof HTMLElement) error.hidden = true;
27761
+ if (error2 instanceof HTMLElement) error2.hidden = true;
26320
27762
  }
26321
27763
  }
26322
27764
  /** @returns {void} */
@@ -26539,6 +27981,7 @@ function registerFieldAdapters() {
26539
27981
  registerFieldAdapters();
26540
27982
  export {
26541
27983
  AccountMenu,
27984
+ AppIcon,
26542
27985
  AppSidebar,
26543
27986
  Avatar,
26544
27987
  Breadcrumb,
@@ -26562,6 +28005,7 @@ export {
26562
28005
  Field,
26563
28006
  FieldUpload,
26564
28007
  Fieldset,
28008
+ Filter,
26565
28009
  Finder,
26566
28010
  Form,
26567
28011
  Grid,
@@ -26602,6 +28046,7 @@ export {
26602
28046
  __,
26603
28047
  addDays,
26604
28048
  addMonths,
28049
+ appIcon,
26605
28050
  appendCompact,
26606
28051
  appendCompactChild,
26607
28052
  appendContent,
@@ -26621,6 +28066,7 @@ export {
26621
28066
  clampRange,
26622
28067
  clampSize,
26623
28068
  cloneChartValue,
28069
+ cloneFilterAst,
26624
28070
  compactElement,
26625
28071
  configureIcons,
26626
28072
  copyButton,
@@ -26631,12 +28077,16 @@ export {
26631
28077
  defineElements,
26632
28078
  describe,
26633
28079
  downloadBlob,
28080
+ emptyFilterAst,
26634
28081
  emptyState,
26635
28082
  escapeRegExp,
26636
28083
  faFamilies,
26637
28084
  faIconClasses,
26638
28085
  faNames,
26639
28086
  faStyles,
28087
+ filterCondition,
28088
+ filterGroup,
28089
+ filterOperators,
26640
28090
  fireCompactEvent,
26641
28091
  focusTrap,
26642
28092
  formatCurrency,
@@ -26676,6 +28126,7 @@ export {
26676
28126
  paginationRange,
26677
28127
  parseCompactTag,
26678
28128
  parseDate,
28129
+ parseFilterAst,
26679
28130
  parseIconSpec,
26680
28131
  parseNumber,
26681
28132
  parseRangeText,
@@ -26687,6 +28138,7 @@ export {
26687
28138
  rankLauncherItems,
26688
28139
  registerFieldAdapters,
26689
28140
  registerIcons,
28141
+ reorderTableRows,
26690
28142
  resolveElement,
26691
28143
  resolveSize,
26692
28144
  rovingTabindex,
@@ -26703,6 +28155,7 @@ export {
26703
28155
  stack,
26704
28156
  stepPrecision,
26705
28157
  storage,
28158
+ stringifyFilterAst,
26706
28159
  throttle,
26707
28160
  toArray,
26708
28161
  toCsv,
@@ -26714,5 +28167,6 @@ export {
26714
28167
  uniqueBy,
26715
28168
  useBuiltinIcons,
26716
28169
  useFontAwesome,
28170
+ validateFilterAst,
26717
28171
  zeyosService
26718
28172
  };