@zeyos/zx 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/DESIGN-SYSTEM.md +87 -71
  3. package/README.md +16 -37
  4. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  6. package/dist/types/components/card/card.d.ts +128 -0
  7. package/dist/types/components/chart/chart.d.ts +1 -0
  8. package/dist/types/components/filter/filter-model.d.ts +87 -0
  9. package/dist/types/components/filter/filter.d.ts +331 -0
  10. package/dist/types/components/launcher/launcher.d.ts +65 -6
  11. package/dist/types/components/select/select.d.ts +97 -4
  12. package/dist/types/components/table/table.d.ts +107 -3
  13. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  14. package/dist/types/core/dom.d.ts +9 -1
  15. package/dist/types/core/util.d.ts +9 -0
  16. package/dist/types/index.d.ts +6 -2
  17. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  18. package/dist/types/zeyos/icons.d.ts +24 -2
  19. package/dist/types/zeyos/index.d.ts +3 -2
  20. package/dist/types/zeyos/launcher.d.ts +147 -0
  21. package/dist/types/zeyos/select.d.ts +41 -0
  22. package/dist/zx-zeyos.esm.js +2018 -113
  23. package/dist/zx-zeyos.esm.min.js +1 -1
  24. package/dist/zx.css +815 -16
  25. package/dist/zx.esm.js +2661 -338
  26. package/dist/zx.esm.min.js +3 -3
  27. package/dist/zx.global.js +2661 -338
  28. package/dist/zx.global.min.js +3 -3
  29. package/dist/zx.min.css +1 -1
  30. package/docs/api.json +602 -110
  31. package/docs/llms.md +181 -123
  32. package/docs/llms.txt +24 -21
  33. package/package.json +8 -17
  34. package/src/components/app-icon/app-icon.css +105 -0
  35. package/src/components/app-icon/app-icon.js +134 -0
  36. package/src/components/app-sidebar/app-sidebar.css +12 -0
  37. package/src/components/app-sidebar/app-sidebar.js +310 -110
  38. package/src/components/card/card.css +149 -0
  39. package/src/components/card/card.js +285 -0
  40. package/src/components/chart/chart.js +8 -3
  41. package/src/components/filter/filter-model.js +226 -0
  42. package/src/components/filter/filter.css +147 -0
  43. package/src/components/filter/filter.js +554 -0
  44. package/src/components/grid/grid.css +1 -1
  45. package/src/components/grid/grid.js +14 -4
  46. package/src/components/launcher/launcher.css +113 -3
  47. package/src/components/launcher/launcher.js +234 -26
  48. package/src/components/message/message.css +36 -3
  49. package/src/components/select/field-adapter.js +9 -4
  50. package/src/components/select/select.css +161 -10
  51. package/src/components/select/select.js +361 -48
  52. package/src/components/table/table.css +85 -0
  53. package/src/components/table/table.js +426 -31
  54. package/src/components/tag-picker/tag-picker.css +19 -0
  55. package/src/components/tag-picker/tag-picker.js +42 -5
  56. package/src/core/dom.js +21 -1
  57. package/src/core/util.js +20 -0
  58. package/src/index.js +9 -2
  59. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  60. package/src/zeyos/icons.js +38 -20
  61. package/src/zeyos/index.js +8 -2
  62. package/src/zeyos/launcher.js +338 -0
  63. package/src/zeyos/modules.js +1 -0
  64. package/src/zeyos/select.js +116 -1
  65. package/styles/tokens/global.css +1 -1
  66. package/styles/tokens/modules.css +1 -0
  67. package/styles/tokens/semantic.css +9 -0
  68. package/styles/tokens/themes.css +2 -2
  69. package/styles/zx.css +3 -0
  70. package/MIGRATION.md +0 -749
  71. package/dist/types/compat/base.d.ts +0 -156
  72. package/dist/types/compat/globals.d.ts +0 -20
  73. package/dist/types/compat/index.d.ts +0 -128
  74. package/dist/types/compat/map/basic.d.ts +0 -175
  75. package/dist/types/compat/map/data.d.ts +0 -103
  76. package/dist/types/compat/map/date-time.d.ts +0 -80
  77. package/dist/types/compat/map/factory.d.ts +0 -9
  78. package/dist/types/compat/map/form.d.ts +0 -98
  79. package/dist/types/compat/map/helpers.d.ts +0 -12
  80. package/dist/types/compat/map/http.d.ts +0 -36
  81. package/dist/types/compat/map/layout.d.ts +0 -106
  82. package/dist/types/compat/map/options.d.ts +0 -80
  83. package/dist/types/compat/map/select.d.ts +0 -95
  84. package/dist/types/compat/map/stubs.d.ts +0 -32
  85. package/dist/types/compat/map/utilities.d.ts +0 -37
  86. package/dist/types/compat-entry.d.ts +0 -5
  87. package/dist/zx-compat.esm.js +0 -17848
  88. package/dist/zx-compat.esm.min.js +0 -1
  89. package/dist/zx-compat.global.js +0 -29008
  90. package/dist/zx-compat.global.min.js +0 -3
  91. package/src/compat/base.js +0 -291
  92. package/src/compat/globals.js +0 -167
  93. package/src/compat/index.js +0 -84
  94. package/src/compat/map/basic.js +0 -464
  95. package/src/compat/map/data.js +0 -254
  96. package/src/compat/map/date-time.js +0 -177
  97. package/src/compat/map/factory.js +0 -45
  98. package/src/compat/map/form.js +0 -330
  99. package/src/compat/map/helpers.js +0 -46
  100. package/src/compat/map/http.js +0 -117
  101. package/src/compat/map/layout.js +0 -256
  102. package/src/compat/map/options.js +0 -267
  103. package/src/compat/map/select.js +0 -198
  104. package/src/compat/map/stubs.js +0 -26
  105. package/src/compat/map/utilities.js +0 -113
  106. package/src/compat-entry.js +0 -11
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
  }
@@ -267,6 +281,18 @@ function compactElement(compact, properties = null, content = null) {
267
281
  return element;
268
282
  }
269
283
  var __ = compactElement;
284
+ function safeHref(value) {
285
+ if (typeof value !== "string") return null;
286
+ const href = value.trim();
287
+ if (!href) return null;
288
+ try {
289
+ const protocol = new URL(href, globalThis.location?.href ?? "https://zx.invalid/").protocol;
290
+ if (["javascript:", "data:", "vbscript:"].includes(protocol.toLowerCase())) return null;
291
+ } catch {
292
+ return null;
293
+ }
294
+ return href;
295
+ }
270
296
  function applyCompactProperties(element, properties) {
271
297
  for (const name in properties ?? {}) {
272
298
  const value = properties[name];
@@ -698,9 +724,9 @@ var Component = class extends EventTarget {
698
724
  let rendered;
699
725
  try {
700
726
  rendered = h.scope(this.refs, () => this.render());
701
- } catch (error) {
727
+ } catch (error2) {
702
728
  this.#abort.abort();
703
- throw error;
729
+ throw error2;
704
730
  }
705
731
  if (this.#created) {
706
732
  const root = isElement(rendered) ? rendered : this.el;
@@ -991,8 +1017,8 @@ var Field = class _Field extends Component {
991
1017
  /** @type {Readonly<FieldOptions>} */
992
1018
  this.options
993
1019
  ));
994
- } catch (error) {
995
- const message = error instanceof Error ? error.message : String(error);
1020
+ } catch (error2) {
1021
+ const message = error2 instanceof Error ? error2.message : String(error2);
996
1022
  console.warn(`[zx.Field] Adapter "${type}" failed: ${message}`);
997
1023
  this.adapter = errorAdapter(message, this.controlId);
998
1024
  }
@@ -4975,8 +5001,8 @@ var FieldUpload = class extends Component {
4975
5001
  return response;
4976
5002
  } catch (caught) {
4977
5003
  if (runId !== this._runId) return null;
4978
- const error = caught instanceof Error ? caught : new Error(String(caught));
4979
- if (error.name === "AbortError") {
5004
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
5005
+ if (error2.name === "AbortError") {
4980
5006
  this._finishAbort();
4981
5007
  return null;
4982
5008
  }
@@ -4985,8 +5011,8 @@ var FieldUpload = class extends Component {
4985
5011
  this._transportController = null;
4986
5012
  this._abort.hidden = true;
4987
5013
  this._setState("error");
4988
- this._status.textContent = error.message;
4989
- this.emit("error", { error });
5014
+ this._status.textContent = error2.message;
5015
+ this.emit("error", { error: error2 });
4990
5016
  return null;
4991
5017
  }
4992
5018
  }
@@ -5134,10 +5160,10 @@ var FieldUpload = class extends Component {
5134
5160
  this.emit("abort", {});
5135
5161
  }
5136
5162
  /** @param {Error} error @returns {void} */
5137
- _selectionError(error) {
5163
+ _selectionError(error2) {
5138
5164
  this._setState("error");
5139
- this._status.textContent = error.message;
5140
- this.emit("error", { error });
5165
+ this._status.textContent = error2.message;
5166
+ this.emit("error", { error: error2 });
5141
5167
  }
5142
5168
  /** @returns {void} */
5143
5169
  _renderPreview() {
@@ -5220,9 +5246,9 @@ function parseResponse(text4) {
5220
5246
  }
5221
5247
  function abortError() {
5222
5248
  if (typeof DOMException === "function") return new DOMException("Upload aborted", "AbortError");
5223
- const error = new Error("Upload aborted");
5224
- error.name = "AbortError";
5225
- return error;
5249
+ const error2 = new Error("Upload aborted");
5250
+ error2.name = "AbortError";
5251
+ return error2;
5226
5252
  }
5227
5253
 
5228
5254
  // src/components/field-upload/field-adapter.js
@@ -6348,8 +6374,8 @@ var Rating = class extends Component {
6348
6374
  const shown = this._preview ?? this._value;
6349
6375
  const symbols = this.refs.symbols.querySelectorAll(".zx-rating__symbol");
6350
6376
  symbols.forEach((symbol, index) => {
6351
- const filled = Math.min(1, Math.max(0, shown - index));
6352
- 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}%`);
6353
6379
  symbol.toggleAttribute("data-preview", this._preview !== null);
6354
6380
  });
6355
6381
  this.el.dataset.value = String(shown);
@@ -6617,17 +6643,33 @@ var PRIORITY_KEYS = [
6617
6643
  "priority.high",
6618
6644
  "priority.highest"
6619
6645
  ];
6620
- var PRIORITY_FALLBACKS = ["Lowest", "Low", "Normal", "High", "Highest"];
6646
+ var PRIORITY_FALLBACKS = ["Lowest", "Low", "Medium", "High", "Highest"];
6647
+ var DEFAULT_STATUSES = Object.freeze([
6648
+ ["backlog", "status.backlog", "Backlog", "dotted", "muted"],
6649
+ ["todo", "status.todo", "Todo", "ring", "neutral"],
6650
+ ["in-progress", "status.inProgress", "In Progress", "progress", "warning"],
6651
+ ["in-review", "status.inReview", "In Review", "review", "info"],
6652
+ ["ready", "status.ready", "Ready to Merge", "ready", "accent"],
6653
+ ["waiting", "status.waiting", "Waiting for feedback", "waiting", "warning"],
6654
+ ["done", "status.done", "Done", "done", "info"],
6655
+ ["canceled", "status.canceled", "Canceled", "canceled", "muted"],
6656
+ ["duplicate", "status.duplicate", "Duplicate", "duplicate", "muted"]
6657
+ ]);
6621
6658
  var Select = class _Select extends Component {
6622
6659
  static cssName = "select";
6623
6660
  /** @type {SelectOptions} */
6624
6661
  static defaults = {
6625
6662
  items: [],
6626
6663
  fixedItems: [],
6664
+ fixedLabel: null,
6627
6665
  valueKey: "ID",
6628
6666
  labelKey: "name",
6629
6667
  renderItem: null,
6630
6668
  renderValue: null,
6669
+ renderValueAdornment: null,
6670
+ renderNone: null,
6671
+ noneLabel: null,
6672
+ actions: [],
6631
6673
  value: null,
6632
6674
  disabled: false,
6633
6675
  placeholder: "",
@@ -6645,6 +6687,7 @@ var Select = class _Select extends Component {
6645
6687
  this._visibleItems = [];
6646
6688
  this._fixedItems = [];
6647
6689
  this._visibleFixed = [];
6690
+ this._actions = [];
6648
6691
  this._selected = null;
6649
6692
  this._optionEntries = [];
6650
6693
  this._activeIndex = -1;
@@ -6656,8 +6699,7 @@ var Select = class _Select extends Component {
6656
6699
  this._position = null;
6657
6700
  this._createdRoot = false;
6658
6701
  this._root = null;
6659
- this._originalChildren = [];
6660
- this._originalAttributes = /* @__PURE__ */ new Map();
6702
+ this._snapshot = null;
6661
6703
  this._createdRoot = this.el === null;
6662
6704
  const root = (
6663
6705
  /** @type {HTMLElement} */
@@ -6665,10 +6707,7 @@ var Select = class _Select extends Component {
6665
6707
  );
6666
6708
  this.el = root;
6667
6709
  this._root = root;
6668
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6669
- this._rememberAttribute(root, "data-state");
6670
- this._rememberAttribute(root, "data-loading");
6671
- this._rememberAttribute(root, "aria-disabled");
6710
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6672
6711
  const listId = uid("zx-select-list");
6673
6712
  const input = h("input", {
6674
6713
  ref: "input",
@@ -6683,6 +6722,12 @@ var Select = class _Select extends Component {
6683
6722
  placeholder: String(this.options.placeholder ?? ""),
6684
6723
  readOnly: !this.options.filter
6685
6724
  });
6725
+ const valueAdornment = h("span", {
6726
+ ref: "valueAdornment",
6727
+ class: "zx-select__value-adornment",
6728
+ ariaHidden: "true",
6729
+ hidden: true
6730
+ });
6686
6731
  const clear = h("button", {
6687
6732
  ref: "clear",
6688
6733
  class: "zx-icon-btn zx-select__clear",
@@ -6708,6 +6753,7 @@ var Select = class _Select extends Component {
6708
6753
  const control = h(
6709
6754
  "div",
6710
6755
  { ref: "control", class: "zx-select__control" },
6756
+ valueAdornment,
6711
6757
  input,
6712
6758
  clear,
6713
6759
  loading,
@@ -6730,9 +6776,10 @@ var Select = class _Select extends Component {
6730
6776
  this._visibleItems = this._items.slice();
6731
6777
  this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6732
6778
  this._visibleFixed = this._fixedItems.slice();
6779
+ this._actions = normalizeSelectActions(this.options.actions);
6733
6780
  this._disabled = Boolean(this.options.disabled);
6734
6781
  this._handleTypeahead = typeahead(
6735
- () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
6782
+ () => this._optionEntries.filter((entry) => !entry.clear && !entry.action).map((entry) => entry.element),
6736
6783
  (_option, index) => {
6737
6784
  if (!this._open) this.open();
6738
6785
  this._setActive(this._itemEntryIndex(index));
@@ -6827,6 +6874,13 @@ var Select = class _Select extends Component {
6827
6874
  this._position?.update();
6828
6875
  return this;
6829
6876
  }
6877
+ /** Replaces command rows without changing the selected value. @param {SelectAction[]} actions @returns {this} */
6878
+ setActions(actions2) {
6879
+ this._actions = normalizeSelectActions(actions2);
6880
+ this._renderOptions();
6881
+ this._position?.update();
6882
+ return this;
6883
+ }
6830
6884
  /**
6831
6885
  * Restores the configured initial value.
6832
6886
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -6911,24 +6965,109 @@ var Select = class _Select extends Component {
6911
6965
  value: null,
6912
6966
  items: [],
6913
6967
  renderItem(item) {
6968
+ return priorityContent(item, true);
6969
+ },
6970
+ renderValueAdornment(item) {
6971
+ return priorityIndicator(item.level);
6972
+ }
6973
+ });
6974
+ select.setItems(PRIORITY_KEYS.map((key, level) => {
6975
+ let name = level === 2 ? select.msg("priority.medium") : key;
6976
+ if (name === "priority.medium") name = select._message(key, PRIORITY_FALLBACKS[level]);
6977
+ else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
6978
+ return { ID: level, name, level };
6979
+ }));
6980
+ select.set(requestedValue, { silent: true });
6981
+ return select;
6982
+ }
6983
+ /**
6984
+ * Creates a status picker with shape- and color-distinct workflow indicators.
6985
+ * @param {Element|string|null} target Component target.
6986
+ * @param {SelectOptions & {items?: Array<Record<string, any>>}} [options={}] Select overrides and status items.
6987
+ * @returns {Select}
6988
+ */
6989
+ static status(target, options = {}) {
6990
+ const { items = null, value = null, ...rest } = options;
6991
+ const select = new _Select(target, {
6992
+ ...rest,
6993
+ value: null,
6994
+ items: [],
6995
+ renderItem(item) {
6996
+ return statusContent(item);
6997
+ },
6998
+ renderValueAdornment(item) {
6999
+ return statusIndicator(item);
7000
+ }
7001
+ });
7002
+ const statuses = Array.isArray(items) ? items.map(normalizeStatusItem).filter(Boolean) : DEFAULT_STATUSES.map(([ID, key, fallback, indicator, tone], index) => ({
7003
+ ID,
7004
+ name: select._message(key, fallback),
7005
+ indicator,
7006
+ tone,
7007
+ shortcut: String(index + 1)
7008
+ }));
7009
+ select.setItems(statuses);
7010
+ select.set(value, { silent: true });
7011
+ return select;
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
+ );
6914
7061
  return h(
6915
7062
  "span",
6916
- { class: "zx-select__priority" },
6917
- h("span", {
6918
- class: "zx-select__priority-icon",
6919
- dataset: { level: item.level },
6920
- ariaHidden: "true"
6921
- }),
6922
- h("span", {}, String(item.name))
7063
+ { class: "zx-select__entity" },
7064
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
7065
+ copy
6923
7066
  );
6924
- }
7067
+ }),
7068
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
6925
7069
  });
6926
- select.setItems(PRIORITY_KEYS.map((key, level) => ({
6927
- ID: level,
6928
- name: select._message(key, PRIORITY_FALLBACKS[level]),
6929
- level
6930
- })));
6931
- select.set(requestedValue, { silent: true });
7070
+ select.el.dataset.preset = "entity";
6932
7071
  return select;
6933
7072
  }
6934
7073
  /**
@@ -6943,12 +7082,21 @@ var Select = class _Select extends Component {
6943
7082
  * @returns {Select}
6944
7083
  */
6945
7084
  static permission(target, options = {}) {
6946
- const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
7085
+ const {
7086
+ groups = [],
7087
+ value = true,
7088
+ valueKey = "ID",
7089
+ labelKey = "name",
7090
+ filter = "local",
7091
+ ...rest
7092
+ } = options;
6947
7093
  const loader = typeof groups === "function" ? groups : null;
6948
7094
  const shareable = loader ? [] : groups;
6949
7095
  const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6950
7096
  const select = new _Select(target, {
6951
- filter: loader ?? "local",
7097
+ // An async group loader is the data source and cannot be disconnected by an incidental
7098
+ // `filter` override. Array-backed groups still honor local/disabled filtering.
7099
+ filter: loader ?? filter,
6952
7100
  ...rest,
6953
7101
  value: null,
6954
7102
  items: shareable,
@@ -6972,18 +7120,24 @@ var Select = class _Select extends Component {
6972
7120
  /** @type {HTMLElement} */
6973
7121
  this.el
6974
7122
  );
6975
- if (!this._createdRoot && root) {
6976
- root.replaceChildren(...this._originalChildren);
6977
- for (const [name, value] of this._originalAttributes) {
6978
- if (value === null) root.removeAttribute(name);
6979
- else root.setAttribute(name, value);
6980
- }
6981
- }
7123
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
6982
7124
  super.destroy();
6983
7125
  }
6984
7126
  /** @param {KeyboardEvent} event @returns {void} */
6985
7127
  _onKeydown(event) {
6986
7128
  if (this._disabled) return;
7129
+ if (this._open && isPrintable(event)) {
7130
+ const shortcut = this._optionEntries.findIndex((entry) => !entry.clear && !entry.action && String(
7131
+ /** @type {Record<string, any>|null} */
7132
+ entry.item?.shortcut ?? ""
7133
+ ) === event.key);
7134
+ if (shortcut >= 0) {
7135
+ event.preventDefault();
7136
+ this._setActive(shortcut);
7137
+ this._chooseEntry(this._optionEntries[shortcut]);
7138
+ return;
7139
+ }
7140
+ }
6987
7141
  if (event.key === "ArrowDown") {
6988
7142
  event.preventDefault();
6989
7143
  if (!this._open) this.open();
@@ -7031,6 +7185,7 @@ var Select = class _Select extends Component {
7031
7185
  /** @type {HTMLInputElement} */
7032
7186
  this.refs.input.value
7033
7187
  );
7188
+ this._syncValueAdornment();
7034
7189
  if (!this._open) this.open();
7035
7190
  if (this.options.filter === "local") {
7036
7191
  this._visibleFixed = this._matchFixed(query);
@@ -7054,6 +7209,22 @@ var Select = class _Select extends Component {
7054
7209
  /** @param {SelectOptionEntry|undefined} entry @returns {void} */
7055
7210
  _chooseEntry(entry) {
7056
7211
  if (!entry) return;
7212
+ if (entry.action) {
7213
+ const detail = {
7214
+ id: entry.action.id,
7215
+ action: entry.action,
7216
+ query: (
7217
+ /** @type {HTMLInputElement} */
7218
+ this.refs.input.value
7219
+ ),
7220
+ select: this
7221
+ };
7222
+ const action = this.emit("action", detail);
7223
+ if (!action.defaultPrevented) entry.action.invoke?.(detail);
7224
+ this.close();
7225
+ this.focus();
7226
+ return;
7227
+ }
7057
7228
  this.set(entry.clear ? null : entry.item);
7058
7229
  this.close();
7059
7230
  this.focus();
@@ -7086,14 +7257,19 @@ var Select = class _Select extends Component {
7086
7257
  list.replaceChildren();
7087
7258
  this._optionEntries = [];
7088
7259
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7260
+ const fixed = this._uniqueItems(this._visibleFixed);
7261
+ const items = this._uniqueItems(this._visibleItems, fixed);
7089
7262
  if (this.options.clearable) {
7090
- this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
7263
+ this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7091
7264
  }
7092
- for (const item of this._visibleFixed) this._appendOption(item, false);
7093
- 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) {
7094
7270
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7095
7271
  }
7096
- for (const item of this._visibleItems) {
7272
+ for (const item of items) {
7097
7273
  if (this.options.groupKey) {
7098
7274
  const group = this._read(item, this.options.groupKey);
7099
7275
  if (!Object.is(group, lastGroup)) {
@@ -7106,7 +7282,23 @@ var Select = class _Select extends Component {
7106
7282
  }
7107
7283
  this._appendOption(item, false);
7108
7284
  }
7109
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
7285
+ if (this._actions.length > 0) {
7286
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7287
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7288
+ }
7289
+ let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
7290
+ for (const action of this._actions) {
7291
+ if (action.group && action.group !== actionGroup) {
7292
+ list.append(h("div", {
7293
+ class: "zx-select__group",
7294
+ role: "presentation"
7295
+ }, action.group));
7296
+ actionGroup = action.group;
7297
+ }
7298
+ this._appendAction(action);
7299
+ }
7300
+ }
7301
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7110
7302
  list.append(h("div", {
7111
7303
  class: "zx-select__empty",
7112
7304
  role: "status"
@@ -7118,7 +7310,7 @@ var Select = class _Select extends Component {
7118
7310
  _appendOption(item, clear, label) {
7119
7311
  const selected = clear ? this._selected === null : this._selected !== null && Object.is(this._itemValue(item), this.value);
7120
7312
  const index = this._optionEntries.length;
7121
- const content = clear ? String(label ?? "") : this._renderItem(item);
7313
+ const content = clear && typeof this.options.renderNone === "function" ? normalizeRenderedContent(this.options.renderNone(String(label ?? ""))) : clear ? String(label ?? "") : this._renderItem(item);
7122
7314
  const option = h("div", {
7123
7315
  class: "zx-select__option",
7124
7316
  id: uid("zx-select-option"),
@@ -7128,16 +7320,37 @@ var Select = class _Select extends Component {
7128
7320
  }, h("span", { class: "zx-select__option-content" }, content));
7129
7321
  if (selected) option.append(icon("check", { size: 15 }));
7130
7322
  this.refs.list.append(option);
7131
- this._optionEntries.push({ item, clear, element: option });
7323
+ this._optionEntries.push({ item, clear, action: null, element: option });
7324
+ }
7325
+ /** @param {SelectAction} action @returns {void} */
7326
+ _appendAction(action) {
7327
+ const index = this._optionEntries.length;
7328
+ const visual = selectActionIcon(action.icon);
7329
+ const option = h("div", {
7330
+ class: "zx-select__option zx-select__action",
7331
+ id: uid("zx-select-option"),
7332
+ role: "option",
7333
+ ariaSelected: "false",
7334
+ dataset: { index, kind: "action" }
7335
+ }, h(
7336
+ "span",
7337
+ { class: "zx-select__option-content zx-select__action-content" },
7338
+ visual ? h("span", { class: "zx-select__action-icon", ariaHidden: "true" }, visual) : null,
7339
+ h(
7340
+ "span",
7341
+ { class: "zx-select__action-copy" },
7342
+ h("span", { class: "zx-select__action-label" }, action.label),
7343
+ action.description ? h("span", { class: "zx-select__action-description" }, action.description) : null
7344
+ )
7345
+ ));
7346
+ this.refs.list.append(option);
7347
+ this._optionEntries.push({ item: null, clear: false, action, element: option });
7132
7348
  }
7133
7349
  /** @param {SelectItem|null} item @returns {Node|string} */
7134
7350
  _renderItem(item) {
7135
7351
  if (typeof this.options.renderItem === "function") {
7136
7352
  const rendered = this.options.renderItem(item);
7137
- return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7138
- /** @type {Node} */
7139
- rendered
7140
- ) : String(rendered ?? "");
7353
+ return normalizeRenderedContent(rendered);
7141
7354
  }
7142
7355
  return this._itemLabel(item);
7143
7356
  }
@@ -7208,9 +7421,29 @@ var Select = class _Select extends Component {
7208
7421
  this.refs.input
7209
7422
  );
7210
7423
  input.value = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7424
+ this._syncValueAdornment();
7211
7425
  this.refs.clear.hidden = !this.options.clearable || this._selected === null;
7212
7426
  }
7213
7427
  /** @returns {void} */
7428
+ _syncValueAdornment() {
7429
+ const host = this.refs.valueAdornment;
7430
+ if (!host) return;
7431
+ const selectedLabel = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7432
+ const query = String(
7433
+ /** @type {HTMLInputElement} */
7434
+ this.refs.input?.value ?? ""
7435
+ );
7436
+ if (this._selected === null || typeof this.options.renderValueAdornment !== "function" || Boolean(this.options.filter && query !== selectedLabel)) {
7437
+ host.replaceChildren();
7438
+ host.hidden = true;
7439
+ return;
7440
+ }
7441
+ const rendered = this.options.renderValueAdornment(this._selected);
7442
+ const content = normalizeRenderedContent(rendered);
7443
+ host.replaceChildren(content instanceof Node ? content : document.createTextNode(content));
7444
+ host.hidden = rendered == null || typeof content === "string" && content === "";
7445
+ }
7446
+ /** @returns {void} */
7214
7447
  _syncDisabled() {
7215
7448
  const input = (
7216
7449
  /** @type {HTMLInputElement} */
@@ -7266,6 +7499,16 @@ var Select = class _Select extends Component {
7266
7499
  _matchFixed(query) {
7267
7500
  return matchItems(this._fixedItems, query, this._searchKeys());
7268
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
+ }
7269
7512
  /** @returns {number} */
7270
7513
  _selectedEntryIndex() {
7271
7514
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7277,16 +7520,104 @@ var Select = class _Select extends Component {
7277
7520
  _itemEntryIndex(itemIndex) {
7278
7521
  return itemIndex + (this.options.clearable ? 1 : 0);
7279
7522
  }
7280
- /** @param {string} name @returns {void} */
7281
- _rememberAttribute(element, name) {
7282
- this._originalAttributes.set(name, element.getAttribute(name));
7283
- }
7284
7523
  /** @param {string} key @param {string} fallback @returns {string} */
7285
7524
  _message(key, fallback) {
7286
7525
  const message = this.msg(key);
7287
7526
  return message === key ? fallback : message;
7288
7527
  }
7289
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
+ }
7545
+ function normalizeRenderedContent(rendered) {
7546
+ return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7547
+ /** @type {Node} */
7548
+ rendered
7549
+ ) : String(rendered ?? "");
7550
+ }
7551
+ function normalizeSelectActions(actions2) {
7552
+ if (!Array.isArray(actions2)) return [];
7553
+ return actions2.filter((action) => action && typeof action === "object" && action.id != null && action.label != null).map((action) => ({
7554
+ ...action,
7555
+ label: String(action.label),
7556
+ description: action.description == null ? void 0 : String(action.description),
7557
+ group: action.group == null ? void 0 : String(action.group)
7558
+ }));
7559
+ }
7560
+ function selectActionIcon(visual) {
7561
+ if (typeof visual === "string") return icon(visual);
7562
+ if (visual instanceof Node) return visual.cloneNode(true);
7563
+ return null;
7564
+ }
7565
+ function priorityContent(item, label) {
7566
+ return h(
7567
+ "span",
7568
+ { class: "zx-select__priority" },
7569
+ priorityIndicator(item.level),
7570
+ label ? h("span", {}, String(item.name)) : null
7571
+ );
7572
+ }
7573
+ function priorityIndicator(level) {
7574
+ const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
7575
+ return h("span", {
7576
+ class: "zx-select__priority-icon",
7577
+ dataset: { level: normalized },
7578
+ ariaHidden: "true"
7579
+ }, Array.from({ length: 5 }, (_, index) => h("span", {
7580
+ dataset: { filled: String(index <= normalized) }
7581
+ })));
7582
+ }
7583
+ function normalizeStatusItem(input) {
7584
+ if (!input || typeof input !== "object" || input.ID == null || input.name == null) return null;
7585
+ const indicators = ["dotted", "ring", "progress", "review", "ready", "waiting", "done", "canceled", "duplicate"];
7586
+ const tones = ["muted", "neutral", "warning", "info", "accent", "success", "danger"];
7587
+ return {
7588
+ ...input,
7589
+ name: String(input.name),
7590
+ indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : "ring",
7591
+ tone: tones.includes(String(input.tone)) ? String(input.tone) : "muted",
7592
+ description: input.description == null ? "" : String(input.description),
7593
+ shortcut: input.shortcut == null ? "" : String(input.shortcut)
7594
+ };
7595
+ }
7596
+ function statusContent(item) {
7597
+ return h(
7598
+ "span",
7599
+ { class: "zx-select__status" },
7600
+ statusIndicator(item),
7601
+ h(
7602
+ "span",
7603
+ { class: "zx-select__status-copy" },
7604
+ h("span", { class: "zx-select__status-label" }, String(item.name)),
7605
+ item.description ? h("span", { class: "zx-select__status-description" }, item.description) : null
7606
+ ),
7607
+ item.shortcut ? h("kbd", { class: "zx-select__status-shortcut" }, item.shortcut) : null
7608
+ );
7609
+ }
7610
+ function statusIndicator(item) {
7611
+ const indicator = String(item.indicator ?? "ring");
7612
+ const marker = h("span", {
7613
+ class: "zx-select__status-indicator",
7614
+ dataset: { indicator, tone: String(item.tone ?? "muted") },
7615
+ ariaHidden: "true"
7616
+ });
7617
+ if (indicator === "done") marker.append(icon("check", { size: 8 }));
7618
+ if (indicator === "canceled") marker.append(icon("x", { size: 7 }));
7619
+ return marker;
7620
+ }
7290
7621
  function readItem(item, reader) {
7291
7622
  if (typeof reader === "function") return reader(item);
7292
7623
  if (item === null || typeof item !== "object") return item;
@@ -7299,10 +7630,11 @@ function isPrintable(event) {
7299
7630
  // src/components/select/field-adapter.js
7300
7631
  function registerSelectFieldAdapter() {
7301
7632
  Field.register("zxselect", (field, options) => {
7302
- const select = field.own(new Select(null, {
7303
- ...options.props ?? {},
7304
- value: options.value
7305
- }));
7633
+ const props = options.props ?? {};
7634
+ const preset = props.preset;
7635
+ const selectOptions = { ...props, value: options.value };
7636
+ delete selectOptions.preset;
7637
+ const select = field.own(preset === "priority" ? Select.priority(null, selectOptions) : preset === "status" ? Select.status(null, selectOptions) : new Select(null, selectOptions));
7306
7638
  field.listen(select.el, "change", (event) => event.stopImmediatePropagation());
7307
7639
  select.on("change", (event) => field.emit("change", { value: event.detail.value }));
7308
7640
  return {
@@ -7686,6 +8018,8 @@ var TagPicker = class extends Component {
7686
8018
  values: [],
7687
8019
  valueKey: "ID",
7688
8020
  labelKey: "name",
8021
+ iconKey: "icon",
8022
+ colorKey: "color",
7689
8023
  searchKeys: null,
7690
8024
  filter: "local",
7691
8025
  minQuery: 0,
@@ -8026,12 +8360,13 @@ var TagPicker = class extends Component {
8026
8360
  _renderTags() {
8027
8361
  const tags = this._selected.map((item) => {
8028
8362
  const label = this._labelOf(item);
8029
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
8030
- const tag = h(
8031
- "span",
8032
- { class: "zx-tag-picker__tag" },
8033
- h("span", { class: "zx-tag-picker__tag-label" }, content)
8034
- );
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);
8035
8370
  if (!this._readonly && !this._disabled) {
8036
8371
  tag.append(h("button", {
8037
8372
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -8069,7 +8404,7 @@ var TagPicker = class extends Component {
8069
8404
  h(
8070
8405
  "span",
8071
8406
  { class: "zx-tag-picker__option-label" },
8072
- 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))]
8073
8408
  )
8074
8409
  );
8075
8410
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8294,10 +8629,10 @@ var TagPicker = class extends Component {
8294
8629
  if (sequence !== this._requestSequence) return;
8295
8630
  this._items = Array.isArray(result) ? result.slice() : [];
8296
8631
  this._visible = this._items.slice();
8297
- } catch (error) {
8632
+ } catch (error2) {
8298
8633
  if (sequence !== this._requestSequence) return;
8299
8634
  this._visible = [];
8300
- this.emit("error", { error });
8635
+ this.emit("error", { error: error2 });
8301
8636
  } finally {
8302
8637
  if (sequence === this._requestSequence) {
8303
8638
  this._setLoading(false);
@@ -8390,6 +8725,34 @@ var TagPicker = class extends Component {
8390
8725
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8391
8726
  return String(item ?? "");
8392
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
+ }
8393
8756
  /** @returns {void} */
8394
8757
  _emitChange() {
8395
8758
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9274,38 +9637,38 @@ var Http = class {
9274
9637
  try {
9275
9638
  json2 = JSON.parse(text4);
9276
9639
  } catch (cause) {
9277
- const error = (
9640
+ const error2 = (
9278
9641
  /** @type {Error & {response?: Response}} */
9279
9642
  new Error("Invalid JSON response", { cause })
9280
9643
  );
9281
- error.response = response;
9282
- throw error;
9644
+ error2.response = response;
9645
+ throw error2;
9283
9646
  }
9284
9647
  }
9285
9648
  if (!response.ok) {
9286
9649
  try {
9287
9650
  parseResult(json2);
9288
- } catch (error2) {
9289
- error2.response = response;
9290
- throw error2;
9651
+ } catch (error3) {
9652
+ error3.response = response;
9653
+ throw error3;
9291
9654
  }
9292
- const error = (
9655
+ const error2 = (
9293
9656
  /** @type {Error & {response?: Response}} */
9294
9657
  new Error(`HTTP ${response.status} ${response.statusText}`.trim())
9295
9658
  );
9296
- error.response = response;
9297
- throw error;
9659
+ error2.response = response;
9660
+ throw error2;
9298
9661
  }
9299
9662
  return parseResult(json2);
9300
9663
  } catch (caught) {
9301
- const error = caught instanceof Error ? caught : new Error(String(caught));
9664
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
9302
9665
  if (this.onError) {
9303
9666
  try {
9304
- this.onError(error, { path, options });
9667
+ this.onError(error2, { path, options });
9305
9668
  } catch {
9306
9669
  }
9307
9670
  }
9308
- throw error;
9671
+ throw error2;
9309
9672
  } finally {
9310
9673
  if (timeoutId !== null) clearTimeout(timeoutId);
9311
9674
  }
@@ -10022,9 +10385,9 @@ var Modal = class extends Component {
10022
10385
  this.el.returnValue = "";
10023
10386
  try {
10024
10387
  this._show();
10025
- } catch (error) {
10388
+ } catch (error2) {
10026
10389
  delete this.el.dataset.zxOverlayOrder;
10027
- throw error;
10390
+ throw error2;
10028
10391
  }
10029
10392
  this.el.dataset.state = "open";
10030
10393
  this.emit("open");
@@ -10507,9 +10870,9 @@ var Groupbox = class extends Component {
10507
10870
  }
10508
10871
  try {
10509
10872
  super(normalizedTarget, options);
10510
- } catch (error) {
10873
+ } catch (error2) {
10511
10874
  if (adoption) restoreAdoption(adoption);
10512
- throw error;
10875
+ throw error2;
10513
10876
  }
10514
10877
  this._adoption = adoption;
10515
10878
  }
@@ -11237,7 +11600,11 @@ var Table = class extends Component {
11237
11600
  rowClass: null,
11238
11601
  zebra: true,
11239
11602
  editMode: false,
11240
- hierarchy: false
11603
+ editTrigger: "double",
11604
+ hierarchy: false,
11605
+ rowReorder: false,
11606
+ columnVisibility: false,
11607
+ hiddenColumns: []
11241
11608
  };
11242
11609
  /**
11243
11610
  * Creates a table around a target or creates a new root when target is null.
@@ -11249,12 +11616,14 @@ var Table = class extends Component {
11249
11616
  }
11250
11617
  /** @returns {HTMLElement} Table root. */
11251
11618
  render() {
11619
+ const created = this.el === null;
11252
11620
  const root = (
11253
11621
  /** @type {HTMLElement} */
11254
11622
  this.el ?? h("div")
11255
11623
  );
11256
11624
  this.el = root;
11257
- this._originalChildren = this.el ? Array.from(this.el.childNodes) : null;
11625
+ this._createdRoot = created;
11626
+ this._original = created ? null : snapshotTarget(root);
11258
11627
  this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
11259
11628
  this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
11260
11629
  this._selected = /* @__PURE__ */ new Set();
@@ -11264,6 +11633,7 @@ var Table = class extends Component {
11264
11633
  this._rowElements = /* @__PURE__ */ new Map();
11265
11634
  this._restored = false;
11266
11635
  this._editMode = normalizeEditMode(this.options.editMode);
11636
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
11267
11637
  this._edit = null;
11268
11638
  this._editMessage = null;
11269
11639
  this._stacked = false;
@@ -11275,6 +11645,10 @@ var Table = class extends Component {
11275
11645
  this._rovingCell = null;
11276
11646
  this._rovingMatched = false;
11277
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;
11278
11652
  this._expanded = initialExpanded(this._hierarchy?.expanded);
11279
11653
  if (this._sort && this.options.sortMode === "local") this._sortData();
11280
11654
  if (this._hierarchy?.expanded === true) {
@@ -11296,11 +11670,17 @@ var Table = class extends Component {
11296
11670
  zebra: String(Boolean(this.options.zebra))
11297
11671
  }
11298
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;
11299
11678
  if (this.options.height != null) {
11300
11679
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
11301
11680
  }
11681
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
11302
11682
  if (this._editMode === false) {
11303
- root.replaceChildren(this._scroll);
11683
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
11304
11684
  } else {
11305
11685
  root.dataset.editMode = this._editMode;
11306
11686
  this._editMessage = h("div", {
@@ -11309,7 +11689,7 @@ var Table = class extends Component {
11309
11689
  role: "status",
11310
11690
  ariaLive: "polite"
11311
11691
  });
11312
- root.replaceChildren(this._scroll, this._editMessage);
11692
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
11313
11693
  }
11314
11694
  this._renderColumns();
11315
11695
  this._renderHeader();
@@ -11317,6 +11697,34 @@ var Table = class extends Component {
11317
11697
  this._watchWidth();
11318
11698
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
11319
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
+ }
11320
11728
  if (this._editMode !== false) {
11321
11729
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
11322
11730
  /** @type {KeyboardEvent} */
@@ -11329,8 +11737,8 @@ var Table = class extends Component {
11329
11737
  }
11330
11738
  /**
11331
11739
  * Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
11332
- * shown, and `aria-busy` is set — a lightweight take on the busy/skeleton pattern used by
11333
- * enterprise tables (SAP UI5, Salesforce SLDS). Call `setLoading(false)` (or `setData`) when done.
11740
+ * shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
11741
+ * Call `setLoading(false)` (or `setData`) when done.
11334
11742
  * @param {boolean} [loading=true] Whether the table is loading.
11335
11743
  * @returns {this}
11336
11744
  */
@@ -11432,6 +11840,81 @@ var Table = class extends Component {
11432
11840
  getData() {
11433
11841
  return [...this._data];
11434
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
+ }
11435
11918
  /**
11436
11919
  * Removes all rows.
11437
11920
  * @returns {this}
@@ -11600,14 +12083,17 @@ var Table = class extends Component {
11600
12083
  */
11601
12084
  startEdit(rowId, columnId) {
11602
12085
  if (this._editMode === false) return this;
11603
- const column = this._columns.find((candidate) => candidate.id === columnId);
11604
- if (!column) return this;
12086
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
12087
+ if (!requested) return this;
11605
12088
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
11606
- 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;
11607
12093
  if (this._edit) {
11608
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
11609
- this._edit.columnId = columnId;
11610
- 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);
11611
12097
  return this;
11612
12098
  }
11613
12099
  this.commitEdit();
@@ -11618,7 +12104,7 @@ var Table = class extends Component {
11618
12104
  const row2 = this._data[index];
11619
12105
  const tr = this._rowElements.get(rowId)?.[0];
11620
12106
  if (!tr) return this;
11621
- 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];
11622
12108
  const cells = /* @__PURE__ */ new Map();
11623
12109
  for (const target of targets) {
11624
12110
  const td = cellIn(tr, target.id);
@@ -11627,10 +12113,10 @@ var Table = class extends Component {
11627
12113
  if (entry) cells.set(target.id, entry);
11628
12114
  }
11629
12115
  if (cells.size === 0) return this;
11630
- this._edit = { id: rowId, columnId, row: row2, cells, tr };
12116
+ this._edit = { id: rowId, columnId: activeColumnId, row: row2, cells, tr };
11631
12117
  tr.dataset.editing = "true";
11632
- this.emit("editstart", { row: row2, id: rowId, column, columnId, value: row2?.[columnId] });
11633
- this._focusEditor(columnId);
12118
+ this.emit("editstart", { row: row2, id: rowId, column, columnId: activeColumnId, value: row2?.[activeColumnId] });
12119
+ this._focusEditor(activeColumnId);
11634
12120
  return this;
11635
12121
  }
11636
12122
  /**
@@ -11879,60 +12365,245 @@ var Table = class extends Component {
11879
12365
  this._teardownEdit();
11880
12366
  this._width?.destroy();
11881
12367
  this._width = null;
11882
- if (this.el) delete this.el.dataset.editMode;
11883
- if (!this._restored && this._originalChildren) {
11884
- this.el.replaceChildren(...this._originalChildren);
12368
+ if (!this._restored && !this._createdRoot) {
12369
+ restoreTarget(this.el, this._original);
11885
12370
  this._restored = true;
11886
12371
  }
11887
12372
  super.destroy();
11888
12373
  }
11889
- /** @returns {void} */
11890
- _renderColumns() {
11891
- const fragment2 = document.createDocumentFragment();
11892
- if (this.options.selectable === "multi") {
11893
- fragment2.append(h("col", { class: "zx-table__selection-column" }));
11894
- }
11895
- const fractions = this._columns.map((column) => parseFraction(column.width));
11896
- const total = fractions.reduce((sum, value) => sum + value, 0);
11897
- this._table.dataset.layout = total > 0 ? "fixed" : "auto";
11898
- this._columns.forEach((column, index) => {
11899
- const col = h("col");
11900
- if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
11901
- else if (column.width && column.width !== "auto") col.style.width = String(column.width);
11902
- fragment2.append(col);
11903
- });
11904
- this._colgroup.replaceChildren(fragment2);
12374
+ /** @returns {TableColumn[]} */
12375
+ _visibleColumns() {
12376
+ return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
11905
12377
  }
11906
- /** @returns {void} */
11907
- _renderHeader() {
11908
- const row2 = h("tr", { class: "zx-table__header-row" });
11909
- if (this._growStep) row2.setAttribute("aria-rowindex", "1");
11910
- if (this.options.selectable === "multi") {
11911
- this._selectAll = h("input", {
11912
- class: "zx-table__checkbox zx-table__select-all",
11913
- type: "checkbox",
11914
- ariaLabel: "Select all rows"
11915
- });
11916
- const selectionHeader = h("th", {
11917
- class: "zx-table__selection-header",
11918
- scope: "col"
11919
- }, this._selectAll);
11920
- row2.append(selectionHeader);
11921
- this.listen(this._selectAll, "click", () => {
11922
- if (this._selectAll.checked) {
11923
- for (const dataRow of this._visibleData()) this._selected.add(this._idFor(dataRow));
11924
- } else {
11925
- for (const dataRow of this._visibleData()) this._selected.delete(this._idFor(dataRow));
11926
- }
11927
- this._selectionAnchorId = null;
11928
- this._syncSelection();
11929
- this._emitSelectionChange();
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;
11930
12409
  });
11931
- } else {
11932
- this._selectAll = null;
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
+ }
12553
+ /** @returns {void} */
12554
+ _renderColumns() {
12555
+ const fragment2 = document.createDocumentFragment();
12556
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
12557
+ if (this.options.selectable === "multi") {
12558
+ fragment2.append(h("col", { class: "zx-table__selection-column" }));
12559
+ }
12560
+ const columns = this._visibleColumns();
12561
+ const fractions = columns.map((column) => parseFraction(column.width));
12562
+ const total = fractions.reduce((sum, value) => sum + value, 0);
12563
+ this._table.dataset.layout = total > 0 ? "fixed" : "auto";
12564
+ columns.forEach((column, index) => {
12565
+ const col = h("col");
12566
+ if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
12567
+ else if (column.width && column.width !== "auto") col.style.width = String(column.width);
12568
+ fragment2.append(col);
12569
+ });
12570
+ this._colgroup.replaceChildren(fragment2);
12571
+ }
12572
+ /** @returns {void} */
12573
+ _renderHeader() {
12574
+ const row2 = h("tr", { class: "zx-table__header-row" });
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
+ }));
12581
+ if (this.options.selectable === "multi") {
12582
+ this._selectAll = h("input", {
12583
+ class: "zx-table__checkbox zx-table__select-all",
12584
+ type: "checkbox",
12585
+ ariaLabel: "Select all rows"
12586
+ });
12587
+ const selectionHeader = h("th", {
12588
+ class: "zx-table__selection-header",
12589
+ scope: "col"
12590
+ }, this._selectAll);
12591
+ row2.append(selectionHeader);
12592
+ this.listen(this._selectAll, "click", () => {
12593
+ if (this._selectAll.checked) {
12594
+ for (const dataRow of this._visibleData()) this._selected.add(this._idFor(dataRow));
12595
+ } else {
12596
+ for (const dataRow of this._visibleData()) this._selected.delete(this._idFor(dataRow));
12597
+ }
12598
+ this._selectionAnchorId = null;
12599
+ this._syncSelection();
12600
+ this._emitSelectionChange();
12601
+ });
12602
+ } else {
12603
+ this._selectAll = null;
11933
12604
  }
11934
12605
  this._headers = /* @__PURE__ */ new Map();
11935
- for (const column of this._columns) {
12606
+ for (const column of this._visibleColumns()) {
11936
12607
  const th = h("th", { scope: "col" });
11937
12608
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
11938
12609
  if (align) th.dataset.align = align;
@@ -11972,7 +12643,7 @@ var Table = class extends Component {
11972
12643
  { class: "zx-table__empty-row" },
11973
12644
  h("td", {
11974
12645
  class: "zx-table__empty",
11975
- 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)
11976
12647
  }, this._emptyContent())
11977
12648
  ));
11978
12649
  } else {
@@ -11981,6 +12652,7 @@ var Table = class extends Component {
11981
12652
  });
11982
12653
  }
11983
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)));
11984
12656
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
11985
12657
  this._syncSelectAll();
11986
12658
  this._syncGrowing();
@@ -12024,6 +12696,19 @@ var Table = class extends Component {
12024
12696
  const elements = this._rowElements.get(id) ?? [];
12025
12697
  elements.push(tr);
12026
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
+ }
12027
12712
  if (this.options.selectable === "multi") {
12028
12713
  const checkbox = h("input", {
12029
12714
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -12033,7 +12718,7 @@ var Table = class extends Component {
12033
12718
  });
12034
12719
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
12035
12720
  }
12036
- this._columns.forEach((column) => {
12721
+ this._visibleColumns().forEach((column) => {
12037
12722
  const cell = (
12038
12723
  /** @type {HTMLTableCellElement} */
12039
12724
  h("td")
@@ -12069,7 +12754,7 @@ var Table = class extends Component {
12069
12754
  const content = h("span", { class: "zx-table__cell-content" });
12070
12755
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
12071
12756
  else if (value != null) content.append(document.createTextNode(String(value)));
12072
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
12757
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
12073
12758
  cell.replaceChildren(...content.childNodes);
12074
12759
  return;
12075
12760
  }
@@ -12100,6 +12785,7 @@ var Table = class extends Component {
12100
12785
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12101
12786
  const meta = this._rowMeta.get(rowElement);
12102
12787
  if (!meta) return;
12788
+ if (target.closest(".zx-table__row-handle")) return;
12103
12789
  if (this._edit) {
12104
12790
  this.commitEdit();
12105
12791
  if (this._edit) return;
@@ -12119,6 +12805,14 @@ var Table = class extends Component {
12119
12805
  );
12120
12806
  return;
12121
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
+ }
12122
12816
  this.emit("rowclick", { ...meta, event });
12123
12817
  if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
12124
12818
  this._selected = /* @__PURE__ */ new Set([meta.id]);
@@ -12139,7 +12833,8 @@ var Table = class extends Component {
12139
12833
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12140
12834
  const meta = this._rowMeta.get(rowElement);
12141
12835
  if (!meta) return;
12142
- if (this._editMode !== false) {
12836
+ if (isInteractiveCellContent(target)) return;
12837
+ if (this._editMode !== false && this._editTrigger === "double") {
12143
12838
  const cell = target?.closest('td[data-editable="true"]');
12144
12839
  if (cell && this._tbody.contains(cell)) {
12145
12840
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -12441,13 +13136,14 @@ var Table = class extends Component {
12441
13136
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
12442
13137
  */
12443
13138
  _nextEditableCell(rowIndex, columnId, direction) {
12444
- const columnCount = this._columns.length;
13139
+ const columns = this._visibleColumns();
13140
+ const columnCount = columns.length;
12445
13141
  if (rowIndex < 0 || columnCount === 0) return null;
12446
13142
  const visible = this._visibleData();
12447
13143
  const startingId = this._idFor(this._data[rowIndex]);
12448
13144
  let row2 = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
12449
13145
  if (row2 < 0) return null;
12450
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
13146
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
12451
13147
  const limit = visible.length * columnCount + columnCount;
12452
13148
  for (let step = 0; step < limit; step += 1) {
12453
13149
  column += direction;
@@ -12460,7 +13156,7 @@ var Table = class extends Component {
12460
13156
  }
12461
13157
  if (row2 < 0 || row2 >= visible.length) return null;
12462
13158
  const candidateRow = visible[row2];
12463
- const candidateColumn = this._columns[column];
13159
+ const candidateColumn = columns[column];
12464
13160
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
12465
13161
  return {
12466
13162
  id: this._idFor(candidateRow),
@@ -12703,6 +13399,53 @@ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
12703
13399
  }
12704
13400
  return result;
12705
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
+ }
12706
13449
  function resolveEditable(column, row2) {
12707
13450
  if (!column || typeof column !== "object") return false;
12708
13451
  if (typeof column.editor === "function") {
@@ -12783,7 +13526,8 @@ function hierarchyForest(rows, rowId, parentId) {
12783
13526
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
12784
13527
  const parentOf = /* @__PURE__ */ new Map();
12785
13528
  for (const node of nodes) {
12786
- 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;
12787
13531
  parentOf.set(node, parent === node ? null : parent);
12788
13532
  }
12789
13533
  for (const node of nodes) {
@@ -12812,6 +13556,9 @@ function hierarchyBranchIds(rows, rowId, parentId) {
12812
13556
  function readRowValue(accessor, row2) {
12813
13557
  return typeof accessor === "function" ? accessor(row2) : row2?.[accessor];
12814
13558
  }
13559
+ function sameHierarchyParent(left, right) {
13560
+ return left == null && right == null || Object.is(left, right);
13561
+ }
12815
13562
  function normalizeHierarchy(option, columns) {
12816
13563
  if (!option || typeof option !== "object") return null;
12817
13564
  const parentId = option.parentId;
@@ -12831,6 +13578,23 @@ function normalizeEditMode(mode) {
12831
13578
  function isInsideEditor(target) {
12832
13579
  return Boolean(target?.closest?.(".zx-table__editor"));
12833
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
+ }
12834
13598
  function deferToEditor(target) {
12835
13599
  if (target.closest("[popover]")) return true;
12836
13600
  return target.getAttribute?.("aria-expanded") === "true";
@@ -15242,6 +16006,244 @@ function normalizeAlign2(value) {
15242
16006
  ) : "start";
15243
16007
  }
15244
16008
 
16009
+ // src/components/card/card.js
16010
+ var Card = class extends Component {
16011
+ static cssName = "card";
16012
+ /** @type {Readonly<CardOptions>} */
16013
+ static defaults = {
16014
+ title: "",
16015
+ media: null,
16016
+ content: null,
16017
+ actions: [],
16018
+ footer: null,
16019
+ link: null,
16020
+ variant: "outlined",
16021
+ orientation: "vertical",
16022
+ headingLevel: 3
16023
+ };
16024
+ /**
16025
+ * Creates or enhances one card.
16026
+ * @param {Element|string|null} [target=null] Existing card container, selector, or null.
16027
+ * @param {CardOptions} [options={}] Card options.
16028
+ */
16029
+ constructor(target = null, options = {}) {
16030
+ super(target, options);
16031
+ }
16032
+ /** @returns {HTMLElement} */
16033
+ render() {
16034
+ const created = !this.el;
16035
+ const root = (
16036
+ /** @type {HTMLElement} */
16037
+ this.el ?? h("article")
16038
+ );
16039
+ this.el = root;
16040
+ this._createdRoot = created;
16041
+ this._original = created ? null : snapshotTarget(root);
16042
+ this._cleaned = false;
16043
+ this._title = "";
16044
+ this._link = null;
16045
+ this._actionAbort = null;
16046
+ this._linkAbort = null;
16047
+ const originalContent = created ? [] : Array.from(root.childNodes);
16048
+ try {
16049
+ const headingLevel = normalizeHeadingLevel(this.options.headingLevel);
16050
+ const headingId = uid("zx-card-heading");
16051
+ const heading = h(`h${headingLevel}`, {
16052
+ ref: "heading",
16053
+ class: "zx-card__heading",
16054
+ id: headingId
16055
+ });
16056
+ const media = h("div", { ref: "media", class: "zx-card__media", hidden: true });
16057
+ const actions2 = h("div", {
16058
+ ref: "actions",
16059
+ class: "zx-card__actions",
16060
+ role: "group",
16061
+ ariaLabel: "Card actions",
16062
+ hidden: true
16063
+ });
16064
+ const content = h("div", { ref: "content", class: "zx-card__content", hidden: true });
16065
+ const footer = h("footer", { ref: "footer", class: "zx-card__footer", hidden: true });
16066
+ const header = h("header", { ref: "header", class: "zx-card__header" }, heading, actions2);
16067
+ root.replaceChildren(h(
16068
+ "div",
16069
+ { class: "zx-card__layout" },
16070
+ media,
16071
+ h("div", { class: "zx-card__inner" }, header, content, footer)
16072
+ ));
16073
+ root.dataset.variant = normalizeVariant2(this.options.variant);
16074
+ root.dataset.orientation = normalizeOrientation(this.options.orientation);
16075
+ this.setTitle(this.options.title);
16076
+ this.setMedia(this.options.media);
16077
+ if (this.options.content !== null) this.setContent(this.options.content);
16078
+ else if (originalContent.length) {
16079
+ content.replaceChildren(...originalContent);
16080
+ content.hidden = false;
16081
+ }
16082
+ this.setActions(this.options.actions);
16083
+ this.setFooter(this.options.footer);
16084
+ this.setLink(this.options.link);
16085
+ return root;
16086
+ } catch (error2) {
16087
+ this._actionAbort?.abort();
16088
+ this._linkAbort?.abort();
16089
+ if (!created) restoreTarget(root, this._original);
16090
+ throw error2;
16091
+ }
16092
+ }
16093
+ /** Replaces the card title. @param {string} title Next title. @returns {this} */
16094
+ setTitle(title) {
16095
+ const next = String(title ?? "");
16096
+ if (this._link && !next.trim()) throw new TypeError("A linked Card requires a non-empty title");
16097
+ this._title = next;
16098
+ this._renderHeading();
16099
+ return this;
16100
+ }
16101
+ /** Replaces or removes the media region. @param {CardContent} content Media. @returns {this} */
16102
+ setMedia(content) {
16103
+ replaceCardContent(this.refs.media, content, "Card media");
16104
+ this.refs.media.hidden = content === null;
16105
+ return this;
16106
+ }
16107
+ /** Replaces or clears the main content. @param {CardContent} content Content. @returns {this} */
16108
+ setContent(content) {
16109
+ replaceCardContent(this.refs.content, content, "Card content");
16110
+ this.refs.content.hidden = content === null;
16111
+ return this;
16112
+ }
16113
+ /** Replaces the secondary action controls. @param {CardAction[]} actions Actions. @returns {this} */
16114
+ setActions(actions2) {
16115
+ if (!Array.isArray(actions2)) throw new TypeError("Card actions must be an array");
16116
+ const nextAbort = new AbortController();
16117
+ let controls;
16118
+ try {
16119
+ controls = actions2.map((action) => {
16120
+ if (isElement(action)) return action;
16121
+ if (!action || typeof action !== "object" || Array.isArray(action)) {
16122
+ throw new TypeError("Card actions must be Elements or button descriptors");
16123
+ }
16124
+ const descriptor = { ...action };
16125
+ const onclick = descriptor.onclick;
16126
+ delete descriptor.onclick;
16127
+ const control = button(descriptor);
16128
+ if (onclick !== void 0 && typeof onclick !== "function") {
16129
+ throw new TypeError("Card action onclick must be a function");
16130
+ }
16131
+ if (onclick) control.addEventListener("click", onclick, { signal: nextAbort.signal });
16132
+ return control;
16133
+ });
16134
+ } catch (error2) {
16135
+ nextAbort.abort();
16136
+ throw error2;
16137
+ }
16138
+ this._actionAbort?.abort();
16139
+ this._actionAbort = nextAbort;
16140
+ this.refs.actions.replaceChildren(...controls);
16141
+ this.refs.actions.hidden = controls.length === 0;
16142
+ this._syncHeader();
16143
+ return this;
16144
+ }
16145
+ /** Replaces or removes the footer. @param {CardContent} content Footer. @returns {this} */
16146
+ setFooter(content) {
16147
+ replaceCardContent(this.refs.footer, content, "Card footer");
16148
+ this.refs.footer.hidden = content === null;
16149
+ return this;
16150
+ }
16151
+ /** Sets or removes the title's native primary link. @param {CardLink} link Link. @returns {this} */
16152
+ setLink(link) {
16153
+ const next = normalizeCardLink(link);
16154
+ if (next && !this._title.trim()) throw new TypeError("A linked Card requires a non-empty title");
16155
+ this._link = next;
16156
+ this.el.dataset.linked = next ? "true" : "false";
16157
+ this._renderHeading();
16158
+ return this;
16159
+ }
16160
+ /** Restores an enhanced target exactly, or removes an owned card. @returns {void} */
16161
+ destroy() {
16162
+ if (this._cleaned) return;
16163
+ this._cleaned = true;
16164
+ this._actionAbort?.abort();
16165
+ this._linkAbort?.abort();
16166
+ super.destroy();
16167
+ if (!this._createdRoot) restoreTarget(this.el, this._original);
16168
+ }
16169
+ /** @returns {void} */
16170
+ _renderHeading() {
16171
+ const title = this._title;
16172
+ const nextAbort = new AbortController();
16173
+ const node = this._link ? h("a", {
16174
+ class: "zx-card__primary",
16175
+ href: this._link.href,
16176
+ target: this._link.target,
16177
+ rel: this._link.rel
16178
+ }, title) : h("span", { class: "zx-card__title" }, title);
16179
+ if (this._link?.onclick) node.addEventListener("click", this._link.onclick, { signal: nextAbort.signal });
16180
+ this._linkAbort?.abort();
16181
+ this._linkAbort = nextAbort;
16182
+ this.refs.heading.replaceChildren(node);
16183
+ this.refs.heading.hidden = title === "";
16184
+ if (title) this.el.setAttribute("aria-labelledby", this.refs.heading.id);
16185
+ else this.el.removeAttribute("aria-labelledby");
16186
+ this._syncHeader();
16187
+ }
16188
+ /** @returns {void} */
16189
+ _syncHeader() {
16190
+ if (!this.refs.header) return;
16191
+ this.refs.header.hidden = this._title === "" && this.refs.actions.childElementCount === 0;
16192
+ }
16193
+ };
16194
+ function replaceCardContent(host, content, label) {
16195
+ if (content === null) {
16196
+ host.replaceChildren();
16197
+ return;
16198
+ }
16199
+ if (typeof content === "string") {
16200
+ host.textContent = content;
16201
+ return;
16202
+ }
16203
+ const node = content instanceof Component ? content.toElement() : content;
16204
+ if (node instanceof Node) {
16205
+ host.replaceChildren(node);
16206
+ return;
16207
+ }
16208
+ throw new TypeError(`${label} must be a string, Node, Component, or null`);
16209
+ }
16210
+ function normalizeCardLink(value) {
16211
+ if (value === null) return null;
16212
+ const descriptor = typeof value === "string" ? { href: value } : value;
16213
+ if (!descriptor || typeof descriptor !== "object" || Array.isArray(descriptor)) {
16214
+ throw new TypeError("Card link must be a URL string, descriptor, or null");
16215
+ }
16216
+ const href = safeHref(descriptor.href);
16217
+ if (href === null) {
16218
+ throw new TypeError("Card link requires a safe non-empty href");
16219
+ }
16220
+ if (descriptor.onclick !== void 0 && typeof descriptor.onclick !== "function") {
16221
+ throw new TypeError("Card link onclick must be a function");
16222
+ }
16223
+ const target = descriptor.target == null ? void 0 : String(descriptor.target);
16224
+ let rel = descriptor.rel == null ? "" : String(descriptor.rel).trim();
16225
+ if (target === "_blank" && !/(^|\s)noopener(\s|$)/i.test(rel)) {
16226
+ rel = `${rel} noopener`.trim();
16227
+ }
16228
+ return { href, target, rel: rel || void 0, onclick: descriptor.onclick };
16229
+ }
16230
+ function normalizeVariant2(value) {
16231
+ return ["raised", "filled"].includes(String(value)) ? (
16232
+ /** @type {'raised'|'filled'} */
16233
+ value
16234
+ ) : "outlined";
16235
+ }
16236
+ function normalizeOrientation(value) {
16237
+ return value === "horizontal" ? "horizontal" : "vertical";
16238
+ }
16239
+ function normalizeHeadingLevel(value) {
16240
+ const level = Math.trunc(Number(value));
16241
+ return (
16242
+ /** @type {1|2|3|4|5|6} */
16243
+ level >= 1 && level <= 6 ? level : 3
16244
+ );
16245
+ }
16246
+
15245
16247
  // src/components/panel/panel.js
15246
16248
  var Panel = class extends Component {
15247
16249
  static cssName = "panel";
@@ -18572,6 +19574,106 @@ function count(value) {
18572
19574
  return value.toLocaleString();
18573
19575
  }
18574
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
+ this.el.replaceChildren(
19650
+ h("span", { class: "zx-app-icon__shine", ariaHidden: "true" }),
19651
+ h("span", { class: "zx-app-icon__glyph", ariaHidden: "true" }, visual),
19652
+ badge2
19653
+ );
19654
+ }
19655
+ };
19656
+ function appIcon(options = {}) {
19657
+ return new AppIcon(null, options).toElement();
19658
+ }
19659
+ function resolveVisual(value) {
19660
+ const resolved = typeof value === "function" ? value() : value;
19661
+ if (typeof resolved === "string") return icon(resolved, { size: "1em" });
19662
+ return resolved && typeof resolved === "object" && typeof resolved.nodeType === "number" ? (
19663
+ /** @type {Node} */
19664
+ resolved
19665
+ ) : null;
19666
+ }
19667
+ function cssLength2(value, fallback) {
19668
+ if (typeof value === "number" && Number.isFinite(value)) return `${Math.max(1, value)}px`;
19669
+ const text4 = String(value ?? "").trim();
19670
+ return text4 && !/[;{}]/.test(text4) ? text4 : fallback;
19671
+ }
19672
+ function normalizeGlass(value) {
19673
+ if (value === false || value === "none") return "none";
19674
+ return value === "strong" ? "strong" : "subtle";
19675
+ }
19676
+
18575
19677
  // src/components/launcher/launcher.js
18576
19678
  var Launcher = class extends Component {
18577
19679
  static cssName = "launcher";
@@ -18585,9 +19687,12 @@ var Launcher = class extends Component {
18585
19687
  maxResults: 100,
18586
19688
  placeholder: "Search applications and records",
18587
19689
  label: "Launcher",
19690
+ closeLabel: "Close launcher",
19691
+ resultsLabel: "Launcher results",
18588
19692
  emptyText: "No results",
18589
19693
  loadingText: "Searching\u2026",
18590
- shortcut: "mod+k"
19694
+ shortcut: "mod+k",
19695
+ hints: { move: "Move", open: "Open", close: "Close" }
18591
19696
  };
18592
19697
  /**
18593
19698
  * Creates an owned launcher dialog or enhances an existing `<dialog>` target.
@@ -18616,6 +19721,7 @@ var Launcher = class extends Component {
18616
19721
  this._sources = normalizeSources(this.options.sources);
18617
19722
  this._sourceItems = /* @__PURE__ */ new Map();
18618
19723
  this._query = String(this.options.query ?? "");
19724
+ this._renderedQuery = null;
18619
19725
  this._results = [];
18620
19726
  this._active = -1;
18621
19727
  this._request = null;
@@ -18642,7 +19748,7 @@ var Launcher = class extends Component {
18642
19748
  ref: "close",
18643
19749
  class: "zx-icon-btn zx-launcher__close",
18644
19750
  type: "button",
18645
- ariaLabel: "Close launcher"
19751
+ ariaLabel: this.options.closeLabel
18646
19752
  }, icon("x"));
18647
19753
  const status = h("div", {
18648
19754
  ref: "status",
@@ -18655,16 +19761,25 @@ var Launcher = class extends Component {
18655
19761
  class: "zx-launcher__results",
18656
19762
  id: listId,
18657
19763
  role: "listbox",
18658
- ariaLabel: "Launcher results"
19764
+ ariaLabel: this.options.resultsLabel
18659
19765
  });
19766
+ const shortcut = this.options.shortcut === "mod+k" ? h("kbd", {
19767
+ class: "zx-launcher__shortcut",
19768
+ ariaHidden: "true"
19769
+ }, launcherShortcutLabel()) : null;
19770
+ const footer = launcherHints(this.options.hints);
18660
19771
  dialog.setAttribute("aria-label", String(this.options.label));
18661
19772
  dialog.replaceChildren(
18662
19773
  h(
18663
19774
  "div",
18664
19775
  { class: "zx-launcher__surface" },
18665
- h("div", { class: "zx-launcher__search" }, icon("search"), input, close),
19776
+ h("div", {
19777
+ class: "zx-launcher__search",
19778
+ dataset: { shortcut: shortcut ? "true" : "false" }
19779
+ }, icon("search"), input, shortcut, close),
18666
19780
  status,
18667
- results
19781
+ results,
19782
+ footer
18668
19783
  )
18669
19784
  );
18670
19785
  dialog.dataset.state = "closed";
@@ -18688,6 +19803,17 @@ var Launcher = class extends Component {
18688
19803
  if (!option || !results.contains(option)) return;
18689
19804
  this._activate(Number(option.getAttribute("data-launcher-index")), event);
18690
19805
  });
19806
+ this.listen(results, "pointermove", (event) => {
19807
+ const option = (
19808
+ /** @type {Element|null} */
19809
+ event.target?.closest?.("[data-launcher-index]")
19810
+ );
19811
+ if (!option || !results.contains(option)) return;
19812
+ const index = Number(option.getAttribute("data-launcher-index"));
19813
+ if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
19814
+ this._active = index;
19815
+ this._syncActive();
19816
+ });
18691
19817
  this.listen(dialog, "cancel", (event) => {
18692
19818
  event.preventDefault();
18693
19819
  this.close();
@@ -18757,8 +19883,15 @@ var Launcher = class extends Component {
18757
19883
  this._renderResults();
18758
19884
  return this;
18759
19885
  }
18760
- /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
18761
- setSources(sources) {
19886
+ /** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
19887
+ setCurrent(id) {
19888
+ const key = id == null ? null : String(id);
19889
+ this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
19890
+ this._renderResults();
19891
+ return this;
19892
+ }
19893
+ /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
19894
+ setSources(sources) {
18762
19895
  this._abortRequest();
18763
19896
  this._sources = normalizeSources(sources);
18764
19897
  this._sourceItems.clear();
@@ -18792,7 +19925,7 @@ var Launcher = class extends Component {
18792
19925
  async _loadSources() {
18793
19926
  this._queryTimer = null;
18794
19927
  const query = this._query.trim();
18795
- const sources = this._sources.filter((source) => query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
19928
+ const sources = this._sources.filter((source) => visibleForQuery(source.when, query) && query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
18796
19929
  if (sources.length === 0) return;
18797
19930
  const request = new AbortController();
18798
19931
  const sequence = ++this._requestSequence;
@@ -18802,15 +19935,17 @@ var Launcher = class extends Component {
18802
19935
  try {
18803
19936
  const loaded = await source.load(query, { signal: request.signal });
18804
19937
  if (request.signal.aborted || sequence !== this._requestSequence) return;
18805
- this._sourceItems.set(source.id, normalizeItems2(loaded).map((item) => ({
19938
+ this._sourceItems.set(source.id, normalizeItems2(loaded).map((item, itemOrder) => ({
18806
19939
  ...item,
18807
19940
  _source: source.id,
18808
- group: item.group ?? source.label ?? ""
19941
+ _sourceOrder: source.order === "source",
19942
+ group: item.group ?? source.label ?? "",
19943
+ itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
18809
19944
  })));
18810
19945
  this._renderResults();
18811
- } catch (error) {
19946
+ } catch (error2) {
18812
19947
  if (request.signal.aborted || sequence !== this._requestSequence) return;
18813
- this.emit("error", { source, error });
19948
+ this.emit("error", { source, error: error2 });
18814
19949
  }
18815
19950
  }));
18816
19951
  if (request.signal.aborted || sequence !== this._requestSequence) return;
@@ -18834,23 +19969,36 @@ var Launcher = class extends Component {
18834
19969
  }
18835
19970
  /** @returns {void} */
18836
19971
  _renderResults() {
18837
- const remote = orderedLauncherSourceItems(this._sources, this._sourceItems);
19972
+ const queryKey = this._query;
19973
+ const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active] ? String(this._results[this._active].id) : null;
19974
+ const query = this._query.trim();
19975
+ const local = this._items.filter((item) => visibleForQuery(item.when, query));
19976
+ const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
19977
+ const remote = this._sources.flatMap((source) => {
19978
+ const loaded = this._sourceItems.get(String(source.id)) ?? [];
19979
+ return source.order === "source" ? loaded.filter((item) => visibleForQuery(item.when, query)) : rankLauncherItems(
19980
+ loaded.filter((item) => visibleForQuery(item.when, query)),
19981
+ query,
19982
+ this.options.maxResults
19983
+ );
19984
+ });
18838
19985
  const seen = /* @__PURE__ */ new Set();
18839
- const combined = [...this._items.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
19986
+ const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
18840
19987
  const key = String(item.id);
18841
19988
  if (seen.has(key)) return false;
18842
19989
  seen.add(key);
18843
19990
  return true;
18844
- });
18845
- const ranked = rankLauncherItems(combined, this._query, this.options.maxResults);
19991
+ }).slice(0, Math.max(0, Number(this.options.maxResults) || 0));
18846
19992
  const groups = /* @__PURE__ */ new Map();
18847
- for (const item of ranked) {
19993
+ for (const item of combined) {
18848
19994
  const group = String(item.group ?? "");
18849
19995
  if (!groups.has(group)) groups.set(group, []);
18850
19996
  groups.get(group).push(item);
18851
19997
  }
18852
19998
  this._results = [...groups.values()].flat();
18853
- this._active = this._results.findIndex((item) => !item.disabled);
19999
+ this._renderedQuery = queryKey;
20000
+ const preserved = activeId === null ? -1 : this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
20001
+ this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
18854
20002
  const nodes = [];
18855
20003
  let index = 0;
18856
20004
  for (const [group, items] of groups) {
@@ -18858,7 +20006,8 @@ var Launcher = class extends Component {
18858
20006
  if (group) nodes.push(h("div", {
18859
20007
  class: "zx-launcher__group",
18860
20008
  role: "group",
18861
- ariaLabel: group
20009
+ ariaLabel: group,
20010
+ dataset: { layout: items.every((item) => item.kind === "application") ? "grid" : "list" }
18862
20011
  }, h("div", { class: "zx-launcher__group-label", ariaHidden: "true" }, group), options));
18863
20012
  else nodes.push(...options);
18864
20013
  }
@@ -18868,9 +20017,14 @@ var Launcher = class extends Component {
18868
20017
  }
18869
20018
  /** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
18870
20019
  _option(item, index) {
18871
- const tag = item.href ? "a" : "button";
20020
+ const href = safeHref(item.href);
20021
+ const tag = href ? "a" : "button";
18872
20022
  const children = [];
18873
- if (item.icon) children.push(h("span", { class: "zx-launcher__icon" }, icon(item.icon)));
20023
+ const visual = launcherItemIcon(item);
20024
+ if (visual) children.push(h("span", {
20025
+ class: "zx-launcher__icon",
20026
+ ariaHidden: "true"
20027
+ }, visual));
18874
20028
  children.push(h(
18875
20029
  "span",
18876
20030
  { class: "zx-launcher__copy" },
@@ -18883,14 +20037,20 @@ var Launcher = class extends Component {
18883
20037
  h(tag, {
18884
20038
  class: "zx-launcher__option",
18885
20039
  type: tag === "button" ? "button" : null,
18886
- href: item.href ?? null,
20040
+ href,
18887
20041
  target: item.target ?? null,
20042
+ rel: tag === "a" ? "noopener" : null,
18888
20043
  role: "option",
18889
20044
  id: uid("zx-launcher-option"),
18890
20045
  tabIndex: -1,
18891
20046
  ariaSelected: "false",
18892
20047
  ariaDisabled: item.disabled ? "true" : null,
18893
- dataset: { launcherIndex: index, pinned: item.pinned ? "true" : null }
20048
+ ariaCurrent: item.current ? "page" : null,
20049
+ dataset: {
20050
+ launcherIndex: index,
20051
+ pinned: item.pinned !== void 0 && item.pinned !== false ? "true" : null,
20052
+ kind: normalizeLauncherKind(item.kind)
20053
+ }
18894
20054
  }, children)
18895
20055
  );
18896
20056
  }
@@ -18927,6 +20087,17 @@ var Launcher = class extends Component {
18927
20087
  }
18928
20088
  const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
18929
20089
  if (enabled.length === 0) return;
20090
+ if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
20091
+ const next = this._spatialIndex(event.key, enabled);
20092
+ if (next !== null) {
20093
+ event.preventDefault();
20094
+ this._active = next;
20095
+ this._syncActive();
20096
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
20097
+ return;
20098
+ }
20099
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
20100
+ }
18930
20101
  let position2 = enabled.indexOf(this._active);
18931
20102
  if (event.key === "Home" || event.key === "PageUp") position2 = 0;
18932
20103
  else if (event.key === "End" || event.key === "PageDown") position2 = enabled.length - 1;
@@ -18938,6 +20109,15 @@ var Launcher = class extends Component {
18938
20109
  this._syncActive();
18939
20110
  this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest" });
18940
20111
  }
20112
+ /** @param {string} direction @param {number[]} enabled @returns {number|null} */
20113
+ _spatialIndex(direction, enabled) {
20114
+ const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
20115
+ if (!(current instanceof HTMLElement)) return null;
20116
+ if ((direction === "ArrowLeft" || direction === "ArrowRight") && this._results[this._active]?.kind !== "application") return null;
20117
+ const candidates = [...this.refs.results.querySelectorAll("[data-launcher-index]")].filter((option) => enabled.includes(Number(option.getAttribute("data-launcher-index"))) && (direction !== "ArrowLeft" && direction !== "ArrowRight" || option.getAttribute("data-kind") === "application"));
20118
+ const next = findSpatialLauncherOption(candidates, current, direction);
20119
+ return next ? Number(next.getAttribute("data-launcher-index")) : null;
20120
+ }
18941
20121
  /** @param {number} index @param {Event} event @returns {void} */
18942
20122
  _activate(index, event) {
18943
20123
  const item = this._results[index];
@@ -18963,25 +20143,103 @@ var Launcher = class extends Component {
18963
20143
  }
18964
20144
  /** @param {KeyboardEvent} event @returns {void} */
18965
20145
  _onDocumentKeydown(event) {
18966
- if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || !event.metaKey && !event.ctrlKey || event.altKey) return;
20146
+ if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
18967
20147
  const active = document.activeElement;
18968
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable) return;
20148
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement || active instanceof HTMLElement && active.isContentEditable) return;
18969
20149
  const modal = document.querySelector("dialog:modal");
18970
20150
  if (modal && modal !== this.el) return;
18971
20151
  event.preventDefault();
18972
20152
  this.open();
18973
20153
  }
18974
20154
  };
20155
+ function launcherShortcutLabel() {
20156
+ const platform = String(
20157
+ /** @type {any} */
20158
+ navigator.userAgentData?.platform ?? navigator.platform ?? ""
20159
+ );
20160
+ return /mac|iphone|ipad/i.test(platform) ? "\u2318 K" : "Ctrl K";
20161
+ }
20162
+ function launcherHints(hints) {
20163
+ if (hints === false) return null;
20164
+ const labels = hints && typeof hints === "object" ? hints : {};
20165
+ return h(
20166
+ "footer",
20167
+ { class: "zx-launcher__hints", ariaHidden: "true" },
20168
+ h("span", {}, h("kbd", {}, "\u2191\u2193\u2190\u2192"), labels.move ?? "Move"),
20169
+ h("span", {}, h("kbd", {}, "\u21B5"), labels.open ?? "Open"),
20170
+ h("span", {}, h("kbd", {}, "Esc"), labels.close ?? "Close")
20171
+ );
20172
+ }
20173
+ function launcherItemIcon(item) {
20174
+ let visual = item.icon;
20175
+ if (typeof visual === "function") visual = visual(item);
20176
+ if (typeof visual === "string") return icon(visual);
20177
+ if (visual instanceof Node) return visual.cloneNode(true);
20178
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
20179
+ visual.toElement === "function") {
20180
+ const element = (
20181
+ /** @type {any} */
20182
+ visual.toElement()
20183
+ );
20184
+ return element instanceof Node ? element.cloneNode(true) : null;
20185
+ }
20186
+ return null;
20187
+ }
20188
+ function normalizeLauncherKind(kind) {
20189
+ return kind === "application" || kind === "record" ? kind : "action";
20190
+ }
20191
+ function visibleForQuery(when, query) {
20192
+ if (when === "empty") return query === "";
20193
+ if (when === "query") return query !== "";
20194
+ return true;
20195
+ }
20196
+ function findSpatialLauncherOption(options, current, direction) {
20197
+ const origin = current.getBoundingClientRect();
20198
+ if (origin.width === 0 && origin.height === 0) return null;
20199
+ const originX = origin.left + origin.width / 2;
20200
+ const originY = origin.top + origin.height / 2;
20201
+ let best = null;
20202
+ let bestScore = Infinity;
20203
+ for (const option of options) {
20204
+ if (option === current) continue;
20205
+ const rect = option.getBoundingClientRect();
20206
+ const dx = rect.left + rect.width / 2 - originX;
20207
+ const dy = rect.top + rect.height / 2 - originY;
20208
+ let valid = false;
20209
+ let score = Infinity;
20210
+ if (direction === "ArrowLeft") {
20211
+ valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
20212
+ score = Math.abs(dx) + Math.abs(dy) * 4;
20213
+ } else if (direction === "ArrowRight") {
20214
+ valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
20215
+ score = Math.abs(dx) + Math.abs(dy) * 4;
20216
+ } else if (direction === "ArrowUp") {
20217
+ valid = dy < -2;
20218
+ score = Math.abs(dy) * 10 + Math.abs(dx);
20219
+ } else if (direction === "ArrowDown") {
20220
+ valid = dy > 2;
20221
+ score = Math.abs(dy) * 10 + Math.abs(dx);
20222
+ }
20223
+ if (valid && score < bestScore) {
20224
+ best = option;
20225
+ bestScore = score;
20226
+ }
20227
+ }
20228
+ return best;
20229
+ }
18975
20230
  function rankLauncherItems(items, query, limit = 100) {
18976
20231
  const needle = searchable(query).trim();
18977
20232
  const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) })).filter(({ score }) => score < Infinity);
18978
20233
  ranked.sort((a, b) => {
20234
+ const aGroup = finiteOrder(a.item.groupOrder);
20235
+ const bGroup = finiteOrder(b.item.groupOrder);
20236
+ if (aGroup !== bGroup) return aGroup - bGroup;
18979
20237
  if (needle === "") {
18980
20238
  const aPinned = pinOrder(a.item.pinned, a.order);
18981
20239
  const bPinned = pinOrder(b.item.pinned, b.order);
18982
20240
  if (aPinned !== bPinned) return aPinned - bPinned;
18983
20241
  }
18984
- return a.score - b.score || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
20242
+ return a.score - b.score || finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder) || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
18985
20243
  });
18986
20244
  const maximum = Math.max(0, Number(limit) || 0);
18987
20245
  return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
@@ -19005,6 +20263,9 @@ function pinOrder(pinned, order) {
19005
20263
  if (pinned) return order;
19006
20264
  return Number.MAX_SAFE_INTEGER;
19007
20265
  }
20266
+ function finiteOrder(value) {
20267
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
20268
+ }
19008
20269
  function searchable(value) {
19009
20270
  return String(value ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
19010
20271
  }
@@ -19019,15 +20280,23 @@ function compareLauncherText(left, right) {
19019
20280
  }
19020
20281
  function normalizeItems2(items) {
19021
20282
  if (!Array.isArray(items)) return [];
19022
- return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => ({ ...item, label: String(item.label) }));
20283
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => normalizeLauncherItem(item));
20284
+ }
20285
+ function normalizeLauncherItem(item) {
20286
+ const normalized = { ...item, label: String(item.label) };
20287
+ if (item.href == null) return normalized;
20288
+ const href = safeHref(item.href);
20289
+ if (href !== null) normalized.href = href;
20290
+ else {
20291
+ delete normalized.href;
20292
+ if (typeof normalized.invoke !== "function") normalized.disabled = true;
20293
+ }
20294
+ return normalized;
19023
20295
  }
19024
20296
  function normalizeSources(sources) {
19025
20297
  if (!Array.isArray(sources)) return [];
19026
20298
  return sources.filter((source) => source && typeof source === "object" && source.id != null && typeof source.load === "function").map((source) => ({ ...source, id: String(source.id) }));
19027
20299
  }
19028
- function orderedLauncherSourceItems(sources, sourceItems) {
19029
- return sources.flatMap((source) => sourceItems.get(String(source.id)) ?? []);
19030
- }
19031
20300
 
19032
20301
  // src/components/avatar/avatar.js
19033
20302
  var Avatar = class extends Component {
@@ -19446,7 +20715,7 @@ var Tooltip = class extends Component {
19446
20715
  this.#content = this.options.content;
19447
20716
  this.#disabled = Boolean(this.options.disabled);
19448
20717
  this.#describedBy = resolved.getAttribute("aria-describedby");
19449
- this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength2(this.options.maxWidth));
20718
+ this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength3(this.options.maxWidth));
19450
20719
  const trigger = TRIGGERS.has(this.options.trigger) ? this.options.trigger : "both";
19451
20720
  if (trigger === "manual") return;
19452
20721
  if (trigger === "hover" || trigger === "both") {
@@ -19704,7 +20973,7 @@ function appendToken(list, token) {
19704
20973
  if (!tokens.includes(token)) tokens.push(token);
19705
20974
  return tokens.join(" ");
19706
20975
  }
19707
- function cssLength2(value) {
20976
+ function cssLength3(value) {
19708
20977
  return typeof value === "number" && Number.isFinite(value) ? `${value}px` : String(value);
19709
20978
  }
19710
20979
  function isFocusVisible(element) {
@@ -19715,10 +20984,10 @@ function isFocusVisible(element) {
19715
20984
  }
19716
20985
  }
19717
20986
 
19718
- // src/components/app-rail/app-rail.js
19719
- var AppRail = class extends Component {
20987
+ // src/internal/app-rail.js
20988
+ var AppRailPresenter = class extends Component {
19720
20989
  static cssName = "app-rail";
19721
- /** @type {Readonly<AppRailOptions>} */
20990
+ /** @type {Readonly<AppRailPresenterOptions>} */
19722
20991
  static defaults = {
19723
20992
  items: [],
19724
20993
  active: null,
@@ -19728,12 +20997,13 @@ var AppRail = class extends Component {
19728
20997
  header: null,
19729
20998
  footer: null,
19730
20999
  openDelay: 80,
19731
- closeDelay: 160
21000
+ closeDelay: 160,
21001
+ renderIcon: null
19732
21002
  };
19733
21003
  /**
19734
21004
  * Creates or enhances a rail host.
19735
21005
  * @param {Element|string|null} target Rail target.
19736
- * @param {AppRailOptions} [options={}] Rail options.
21006
+ * @param {AppRailPresenterOptions} [options={}] Rail options.
19737
21007
  */
19738
21008
  constructor(target = null, options = {}) {
19739
21009
  super(target, options);
@@ -19883,7 +21153,14 @@ var AppRail = class extends Component {
19883
21153
  index: String(item.id),
19884
21154
  iconOnly: true,
19885
21155
  disclosure: hasChildren,
19886
- disclosureIcon: railDisclosureIcon(this.el.dataset.side)
21156
+ disclosureIcon: railDisclosureIcon(this.el.dataset.side),
21157
+ visual: appNavIcon(item, this.options.renderIcon, {
21158
+ orientation: this.el.dataset.orientation,
21159
+ collapsed: true,
21160
+ side: this.el.dataset.side,
21161
+ level: 1,
21162
+ location: "root"
21163
+ })
19887
21164
  });
19888
21165
  const row2 = h("li", { class: "zx-app-rail__row" }, control);
19889
21166
  this.refs.list.append(row2);
@@ -19924,7 +21201,14 @@ var AppRail = class extends Component {
19924
21201
  index: childKey,
19925
21202
  iconOnly: false,
19926
21203
  disclosure: hasChildren,
19927
- disclosureIcon: nestedDisclosureIcon(this.el.dataset.side)
21204
+ disclosureIcon: nestedDisclosureIcon(this.el.dataset.side),
21205
+ visual: appNavIcon(child, this.options.renderIcon, {
21206
+ orientation: this.el.dataset.orientation,
21207
+ collapsed: true,
21208
+ side: this.el.dataset.side,
21209
+ level: childKey.split("/").length,
21210
+ location: "flyout"
21211
+ })
19928
21212
  });
19929
21213
  list.append(h("li", {}, control));
19930
21214
  if (hasChildren) this._buildFlyout(child, control, childKey, key);
@@ -19978,7 +21262,6 @@ var AppRail = class extends Component {
19978
21262
  this._interaction.listen(trigger, "click", (event) => {
19979
21263
  event.preventDefault();
19980
21264
  if (event instanceof MouseEvent && event.detail === 0) this.openFlyout(key, { focus: true });
19981
- else if (dropdown.isOpen()) this.closeFlyout(key);
19982
21265
  else this.openFlyout(key);
19983
21266
  });
19984
21267
  this._interaction.listen(actualPanel, "pointerenter", () => this._cancelFlyoutTimers(flyout, true));
@@ -20233,14 +21516,43 @@ var RailListenerScope = class extends Component {
20233
21516
  };
20234
21517
  function normalizeAppItems(items) {
20235
21518
  if (!Array.isArray(items)) return [];
20236
- return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => ({
20237
- ...item,
20238
- label: String(item.label),
20239
- children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
20240
- }));
21519
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => {
21520
+ const normalized = {
21521
+ ...item,
21522
+ label: String(item.label),
21523
+ children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
21524
+ };
21525
+ if (item.href == null) return normalized;
21526
+ const href = safeHref(item.href);
21527
+ if (href !== null) normalized.href = href;
21528
+ else {
21529
+ delete normalized.href;
21530
+ if (normalized.children.length === 0 && typeof normalized.invoke !== "function") {
21531
+ normalized.disabled = true;
21532
+ }
21533
+ }
21534
+ return normalized;
21535
+ });
20241
21536
  }
20242
- function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon }) {
20243
- const tag = item.href && !disclosure ? "a" : "button";
21537
+ function appNavIcon(item, renderer, context) {
21538
+ let visual = typeof renderer === "function" ? renderer(item, context) : null;
21539
+ if (visual == null) visual = item.icon;
21540
+ if (typeof visual === "function") visual = visual(context, item);
21541
+ if (typeof visual === "string") return icon(visual);
21542
+ if (visual instanceof Node) return visual.cloneNode(true);
21543
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
21544
+ visual.toElement === "function") {
21545
+ const element = (
21546
+ /** @type {any} */
21547
+ visual.toElement()
21548
+ );
21549
+ return element instanceof Node ? element.cloneNode(true) : null;
21550
+ }
21551
+ return null;
21552
+ }
21553
+ function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon, visual }) {
21554
+ const href = safeHref(item.href);
21555
+ const tag = href && !disclosure ? "a" : "button";
20244
21556
  return (
20245
21557
  /** @type {HTMLElement} */
20246
21558
  h(
@@ -20248,13 +21560,14 @@ function navControl(item, { className, index, iconOnly, disclosure, disclosureIc
20248
21560
  {
20249
21561
  class: className,
20250
21562
  type: tag === "button" ? "button" : null,
20251
- href: tag === "a" ? item.href : null,
21563
+ href: tag === "a" ? href : null,
20252
21564
  target: tag === "a" ? item.target ?? null : null,
21565
+ rel: tag === "a" ? "noopener" : null,
20253
21566
  ariaLabel: iconOnly ? item.label : null,
20254
21567
  ariaDisabled: item.disabled ? "true" : null,
20255
21568
  dataset: { appNavId: index, appNavValueId: String(item.id) }
20256
21569
  },
20257
- item.icon ? h("span", { class: `${className}-icon`, ariaHidden: "true" }, icon(item.icon)) : null,
21570
+ visual ? h("span", { class: `${className}-icon`, ariaHidden: "true" }, visual) : null,
20258
21571
  h("span", { class: `${className}-label` }, item.label),
20259
21572
  item.badge != null ? h("span", { class: `${className}-badge` }, String(item.badge)) : null,
20260
21573
  disclosure ? h("span", { class: `${className}-disclosure`, ariaHidden: "true" }, icon(disclosureIcon)) : null
@@ -20318,47 +21631,28 @@ var AppSidebar = class extends Component {
20318
21631
  active: null,
20319
21632
  expanded: [],
20320
21633
  collapsed: false,
21634
+ collapsible: true,
21635
+ orientation: "vertical",
20321
21636
  side: "left",
20322
21637
  label: "Applications",
20323
21638
  header: null,
20324
21639
  footer: null,
20325
21640
  railHeader: null,
20326
- railFooter: null
21641
+ railFooter: null,
21642
+ openDelay: 80,
21643
+ closeDelay: 160,
21644
+ renderIcon: null
20327
21645
  };
20328
21646
  /**
20329
- * Creates or enhances an application-sidebar host.
20330
- * @param {Element|string|null} target Sidebar target.
20331
- * @param {AppSidebarOptions} [options={}] Sidebar options.
21647
+ * Creates or enhances an application-navigation host.
21648
+ * @param {Element|string|null} [target=null] Navigation target.
21649
+ * @param {AppSidebarOptions} [options={}] Navigation options.
20332
21650
  */
20333
21651
  constructor(target = null, options = {}) {
20334
21652
  super(target, options);
20335
- this.listen(this.refs.rail, "zx-select", (event) => event.stopPropagation());
20336
- const expandButton = h("button", {
20337
- class: "zx-app-sidebar__rail-toggle zx-icon-btn",
20338
- type: "button",
20339
- ariaLabel: "Expand application sidebar"
20340
- }, icon(this.el.dataset.side === "right" ? "chevron-left" : "chevron-right"));
20341
- this._railExpandButton = expandButton;
20342
- const railHeader = h("div", { class: "zx-app-sidebar__rail-header" }, expandButton);
20343
- appendSlot2(railHeader, this.options.railHeader);
20344
- this._rail = new AppRail(this.refs.rail, {
20345
- items: this._items,
20346
- active: this._active,
20347
- orientation: "vertical",
20348
- side: this.el.dataset.side,
20349
- label: this.options.label,
20350
- header: railHeader,
20351
- footer: this.options.railFooter,
20352
- onselect: (event) => {
20353
- const selected = this.emit("select", event.detail);
20354
- if (selected.defaultPrevented) event.preventDefault();
20355
- }
20356
- });
20357
- this.listen(expandButton, "click", () => {
20358
- this.expand();
20359
- });
20360
- this._renderTree();
20361
- this.setCollapsed(Boolean(this.options.collapsed), { silent: true });
21653
+ this._rail = null;
21654
+ this._railExpandButton = null;
21655
+ this._renderMode();
20362
21656
  }
20363
21657
  /** @returns {HTMLElement} */
20364
21658
  render() {
@@ -20376,57 +21670,39 @@ var AppSidebar = class extends Component {
20376
21670
  this._active = this.options.active;
20377
21671
  this._expanded = new Set(Array.isArray(this.options.expanded) ? this.options.expanded.map(String) : []);
20378
21672
  this._collapsed = Boolean(this.options.collapsed);
20379
- root.dataset.side = this.options.side === "right" ? "right" : "left";
20380
- const headerContent = h("div", { class: "zx-app-sidebar__header-content" });
20381
- appendSlot2(headerContent, this.options.header);
20382
- const collapseButton = h("button", {
20383
- ref: "collapse",
20384
- class: "zx-app-sidebar__collapse zx-icon-btn",
20385
- type: "button",
20386
- ariaLabel: "Minimize application sidebar"
20387
- }, icon(root.dataset.side === "right" ? "chevron-right" : "chevron-left"));
20388
- const footer = h("footer", { class: "zx-app-sidebar__footer" });
20389
- appendSlot2(footer, this.options.footer);
20390
- footer.hidden = !footer.hasChildNodes();
20391
- const expanded = h(
20392
- "div",
20393
- { ref: "expanded", class: "zx-app-sidebar__expanded" },
20394
- h("header", { class: "zx-app-sidebar__header" }, headerContent, collapseButton),
20395
- h(
20396
- "nav",
20397
- { class: "zx-app-sidebar__nav", ariaLabel: this.options.label },
20398
- h("ul", { ref: "list", class: "zx-app-sidebar__list" })
20399
- ),
20400
- footer
20401
- );
20402
- const rail = h("div", { ref: "rail", class: "zx-app-sidebar__rail" });
20403
- root.replaceChildren(expanded, rail);
20404
- this.listen(collapseButton, "click", () => {
20405
- this.collapse();
20406
- });
20407
- this.listen(this.refs.list, "click", (event) => this._onTreeClick(event));
21673
+ this._orientation = this.options.orientation === "horizontal" ? "horizontal" : "vertical";
21674
+ this._side = normalizeSide(this._orientation, this.options.side);
21675
+ this._modeAbort = null;
21676
+ root.replaceChildren();
20408
21677
  return root;
20409
21678
  }
20410
- /** Replaces the navigation tree in both expanded and rail presentations. @param {AppNavItem[]} items @returns {this} */
21679
+ /** Replaces the navigation tree in the active presentation. @param {AppNavItem[]} items @returns {this} */
20411
21680
  setItems(items) {
20412
21681
  this._items = normalizeAppItems(items);
20413
21682
  this._reconcileExpanded();
20414
- this._renderTree();
20415
- this._rail.setItems(this._items);
21683
+ if (this._rail) this._rail.setItems(this._items);
21684
+ else this._renderTree();
20416
21685
  return this;
20417
21686
  }
20418
- /** Sets the active id in both presentations without routing. @param {string|number|null} id @returns {this} */
21687
+ /** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
21688
+ getItems() {
21689
+ return this._items.map((item) => ({
21690
+ ...item,
21691
+ children: item.children?.map((child) => ({ ...child }))
21692
+ }));
21693
+ }
21694
+ /** Sets the active id without routing. @param {string|number|null} id @returns {this} */
20419
21695
  setActive(id) {
20420
21696
  this._active = id;
20421
- this._syncActive();
20422
- this._rail.setActive(id);
21697
+ if (this._rail) this._rail.setActive(id);
21698
+ else this._syncActive();
20423
21699
  return this;
20424
21700
  }
20425
21701
  /** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
20426
21702
  setExpanded(ids, { silent = false } = {}) {
20427
21703
  this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
20428
21704
  this._reconcileExpanded();
20429
- this._renderTree();
21705
+ if (!this._rail) this._renderTree();
20430
21706
  if (!silent) this.emit("branchchange", {
20431
21707
  item: null,
20432
21708
  id: null,
@@ -20451,47 +21727,105 @@ var AppSidebar = class extends Component {
20451
21727
  const expanded = !this._expanded.has(key);
20452
21728
  if (expanded) this._expanded.add(key);
20453
21729
  else this._expanded.delete(key);
20454
- this._renderTree();
20455
- if (!silent) this.emit("branchchange", { item, id: item.id, expanded, ids: this.getExpanded() });
21730
+ if (!this._rail) this._renderTree();
21731
+ if (!silent) this.emit("branchchange", {
21732
+ item,
21733
+ id: item.id,
21734
+ expanded,
21735
+ ids: this.getExpanded()
21736
+ });
20456
21737
  return this;
20457
21738
  }
20458
- /** Shows the expanded sidebar presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
21739
+ /** Shows the expanded vertical presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
20459
21740
  expand(options = {}) {
20460
21741
  return this.setCollapsed(false, options);
20461
21742
  }
20462
- /** Shows the minimized AppRail presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
21743
+ /** Shows the minimized presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
20463
21744
  collapse(options = {}) {
20464
21745
  return this.setCollapsed(true, options);
20465
21746
  }
20466
- /** Sets expanded/minimized presentation while preserving branch state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
21747
+ /** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
20467
21748
  setCollapsed(collapsed, { silent = false } = {}) {
20468
- const next = Boolean(collapsed);
20469
- const changed = next !== this._collapsed;
21749
+ const previous = this.isCollapsed();
20470
21750
  const active = document.activeElement;
20471
- const focusRailToggle = next && Boolean(active && this.refs.expanded.contains(active));
20472
- const focusSidebarToggle = !next && this._rail?._containsNode(active);
20473
- if (!next) this._rail?.closeAllFlyouts();
20474
- this._collapsed = next;
20475
- this.el.dataset.collapsed = String(next);
20476
- this.refs.expanded.hidden = next;
20477
- this.refs.rail.hidden = !next;
20478
- if (focusRailToggle) this._railExpandButton?.focus();
20479
- else if (focusSidebarToggle) this.refs.collapse.focus();
20480
- if (changed && !silent) this.emit("collapsechange", { collapsed: next });
20481
- return this;
20482
- }
20483
- /** Reports whether the rail presentation is active. @returns {boolean} */
21751
+ const outgoingOwnedFocus = Boolean(active && (this.el.contains(active) || this._rail?._containsNode(active)));
21752
+ this._collapsed = Boolean(collapsed);
21753
+ const next = this.isCollapsed();
21754
+ if (previous !== next) {
21755
+ this._renderMode();
21756
+ if (outgoingOwnedFocus) {
21757
+ if (next) this._railExpandButton?.focus();
21758
+ else this.refs.collapse?.focus();
21759
+ }
21760
+ if (!silent) this.emit("collapsechange", { collapsed: next });
21761
+ }
21762
+ return this;
21763
+ }
21764
+ /** Reports the effective presentation state; horizontal navigation is always minimized. @returns {boolean} */
20484
21765
  isCollapsed() {
20485
- return this._collapsed;
21766
+ return this._orientation === "horizontal" || this._collapsed;
21767
+ }
21768
+ /**
21769
+ * Changes navigation flow/edge without replacing state.
21770
+ * @param {{orientation?: 'vertical'|'horizontal', side?: 'left'|'right'|'top'|'bottom'}} layout
21771
+ * @param {{silent?: boolean}} [options]
21772
+ * @returns {this}
21773
+ */
21774
+ setLayout(layout = {}, { silent = false } = {}) {
21775
+ const orientation = layout.orientation === "horizontal" ? "horizontal" : layout.orientation === "vertical" ? "vertical" : this._orientation;
21776
+ const side = normalizeSide(orientation, layout.side ?? this._side);
21777
+ if (orientation === this._orientation && side === this._side) return this;
21778
+ const previous = this.isCollapsed();
21779
+ const active = document.activeElement;
21780
+ const outgoingOwnedFocus = Boolean(active && (this.el.contains(active) || this._rail?._containsNode(active)));
21781
+ const focusedId = outgoingOwnedFocus && active instanceof Element ? active.closest("[data-app-nav-id]")?.getAttribute("data-app-nav-id") ?? null : null;
21782
+ this._orientation = orientation;
21783
+ this._side = side;
21784
+ this._renderMode();
21785
+ const next = this.isCollapsed();
21786
+ if (outgoingOwnedFocus) {
21787
+ const replacement = focusedId === null ? null : [...this.el.querySelectorAll("[data-app-nav-id]")].find((control) => control.getAttribute("data-app-nav-id") === focusedId) ?? null;
21788
+ if (replacement instanceof HTMLElement) replacement.focus();
21789
+ else if (next) this.focus();
21790
+ else if (this.refs.collapse instanceof HTMLElement && !this.refs.collapse.hidden) {
21791
+ this.refs.collapse.focus();
21792
+ } else visibleTreeControls(this.refs.list)[0]?.focus();
21793
+ }
21794
+ if (previous !== next && !silent) this.emit("collapsechange", { collapsed: next });
21795
+ return this;
20486
21796
  }
20487
- /** Returns the composed minimized rail. @returns {AppRail} */
20488
- getRail() {
20489
- return this._rail;
21797
+ /** Opens a minimized descendant flyout. @param {string|number} id @param {{focus?: boolean}} [options={}] @returns {this} */
21798
+ openFlyout(id, options = {}) {
21799
+ this._rail?.openFlyout(id, options);
21800
+ return this;
21801
+ }
21802
+ /** Closes a minimized descendant flyout. @param {string|number} id @returns {this} */
21803
+ closeFlyout(id) {
21804
+ this._rail?.closeFlyout(id);
21805
+ return this;
20490
21806
  }
20491
- /** Destroys the rail and restores/removes the sidebar host. @returns {void} */
21807
+ /** Closes every minimized flyout. @returns {this} */
21808
+ closeAllFlyouts() {
21809
+ this._rail?.closeAllFlyouts();
21810
+ return this;
21811
+ }
21812
+ /** Reports whether a minimized flyout is open. @param {string|number} id @returns {boolean} */
21813
+ isFlyoutOpen(id) {
21814
+ return this._rail?.isFlyoutOpen(id) ?? false;
21815
+ }
21816
+ /** Focuses the first navigation action. @returns {this} */
21817
+ focus() {
21818
+ if (this._rail) this._rail.focus();
21819
+ else visibleTreeControls(this.refs.list)[0]?.focus();
21820
+ return this;
21821
+ }
21822
+ /** Destroys flyouts and restores/removes the host. @returns {void} */
20492
21823
  destroy() {
20493
21824
  const original = this._original;
20494
- this._rail.destroy();
21825
+ this._modeAbort?.abort();
21826
+ this._modeAbort = null;
21827
+ this._rail?.destroy();
21828
+ this._rail = null;
20495
21829
  super.destroy();
20496
21830
  if (!this._createdRoot && original) {
20497
21831
  for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
@@ -20499,8 +21833,99 @@ var AppSidebar = class extends Component {
20499
21833
  this.el.replaceChildren(...original.children);
20500
21834
  }
20501
21835
  }
21836
+ /** Mounts exactly one expanded tree or minimized flyout presentation. @returns {void} */
21837
+ _renderMode() {
21838
+ this._modeAbort?.abort();
21839
+ this._modeAbort = new AbortController();
21840
+ this._rail?.destroy();
21841
+ this._rail = null;
21842
+ this._railExpandButton = null;
21843
+ for (const key of ["expanded", "list", "collapse", "rail"]) delete this.refs[key];
21844
+ this.el.replaceChildren();
21845
+ this.el.dataset.orientation = this._orientation;
21846
+ this.el.dataset.side = this._side;
21847
+ this.el.dataset.collapsed = String(this.isCollapsed());
21848
+ if (this.isCollapsed()) this._renderRail();
21849
+ else this._renderExpanded();
21850
+ }
21851
+ /** @returns {void} */
21852
+ _renderExpanded() {
21853
+ const headerContent = h("div", { class: "zx-app-sidebar__header-content" });
21854
+ appendSlot2(headerContent, this.options.header);
21855
+ const collapseButton = h("button", {
21856
+ class: "zx-app-sidebar__collapse zx-icon-btn",
21857
+ type: "button",
21858
+ ariaLabel: "Minimize application sidebar"
21859
+ }, icon(this._side === "right" ? "chevron-right" : "chevron-left"));
21860
+ collapseButton.hidden = !this.options.collapsible;
21861
+ const footer = h("footer", { class: "zx-app-sidebar__footer" });
21862
+ appendSlot2(footer, this.options.footer);
21863
+ footer.hidden = !footer.hasChildNodes();
21864
+ const list = h("ul", { class: "zx-app-sidebar__list" });
21865
+ const expanded = h(
21866
+ "div",
21867
+ { class: "zx-app-sidebar__expanded" },
21868
+ h("header", { class: "zx-app-sidebar__header" }, headerContent, collapseButton),
21869
+ h("nav", { class: "zx-app-sidebar__nav", ariaLabel: this.options.label }, list),
21870
+ footer
21871
+ );
21872
+ this.refs.expanded = expanded;
21873
+ this.refs.list = list;
21874
+ this.refs.collapse = collapseButton;
21875
+ this.el.append(expanded);
21876
+ this._listenMode(collapseButton, "click", () => this.collapse());
21877
+ this._listenMode(list, "click", (event) => this._onTreeClick(event));
21878
+ this._listenMode(list, "keydown", (event) => this._onTreeKeydown(
21879
+ /** @type {KeyboardEvent} */
21880
+ event
21881
+ ));
21882
+ this._renderTree();
21883
+ }
21884
+ /** @returns {void} */
21885
+ _renderRail() {
21886
+ const railHost = h("nav", { class: "zx-app-sidebar__rail" });
21887
+ this.el.append(railHost);
21888
+ this.refs.rail = railHost;
21889
+ const showToggle = this._orientation === "vertical" && Boolean(this.options.collapsible);
21890
+ const railHeader = h("div", { class: "zx-app-sidebar__rail-header" });
21891
+ if (showToggle) {
21892
+ const expandButton = h("button", {
21893
+ class: "zx-app-sidebar__rail-toggle zx-icon-btn",
21894
+ type: "button",
21895
+ ariaLabel: "Expand application sidebar"
21896
+ }, icon(this._side === "right" ? "chevron-left" : "chevron-right"));
21897
+ railHeader.append(expandButton);
21898
+ this._railExpandButton = expandButton;
21899
+ this._listenMode(expandButton, "click", () => this.expand());
21900
+ }
21901
+ appendSlot2(railHeader, this.options.railHeader);
21902
+ this._rail = new AppRailPresenter(railHost, {
21903
+ items: this._items,
21904
+ active: this._active,
21905
+ orientation: this._orientation,
21906
+ side: this._side,
21907
+ label: this.options.label,
21908
+ header: railHeader.hasChildNodes() ? railHeader : null,
21909
+ footer: this.options.railFooter,
21910
+ openDelay: this.options.openDelay,
21911
+ closeDelay: this.options.closeDelay,
21912
+ renderIcon: this.options.renderIcon,
21913
+ onselect: (event) => {
21914
+ const selected = this.emit("select", event.detail);
21915
+ if (selected.defaultPrevented) event.preventDefault();
21916
+ },
21917
+ onflyoutchange: (event) => this.emit("flyoutchange", event.detail)
21918
+ });
21919
+ this._listenMode(railHost, "zx-select", (event) => event.stopPropagation());
21920
+ this._listenMode(railHost, "zx-flyoutchange", (event) => event.stopPropagation());
21921
+ }
21922
+ /** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
21923
+ _listenMode(target, type, listener) {
21924
+ target.addEventListener(type, listener, { signal: this._modeAbort.signal });
21925
+ }
20502
21926
  /** @returns {void} */
20503
21927
  _renderTree() {
21928
+ if (!this.refs.list) return;
20504
21929
  const focused = this.refs.list.contains(document.activeElement) ? (
20505
21930
  /** @type {Element} */
20506
21931
  document.activeElement.closest("[data-app-nav-id]")?.getAttribute("data-app-nav-id")
@@ -20519,7 +21944,14 @@ var AppSidebar = class extends Component {
20519
21944
  const key = String(item.id);
20520
21945
  const expanded = hasChildren && this._expanded.has(key);
20521
21946
  const childId = hasChildren ? uid("zx-app-sidebar-branch") : null;
20522
- const control = navControl2(item, hasChildren, expanded, childId);
21947
+ const visual = appNavIcon(item, this.options.renderIcon, {
21948
+ orientation: this._orientation,
21949
+ collapsed: false,
21950
+ side: this._side,
21951
+ level,
21952
+ location: level === 1 ? "root" : "inline"
21953
+ });
21954
+ const control = navControl2(item, hasChildren, expanded, childId, visual);
20523
21955
  const row2 = h("li", {
20524
21956
  class: "zx-app-sidebar__row",
20525
21957
  dataset: { level }
@@ -20533,7 +21965,7 @@ var AppSidebar = class extends Component {
20533
21965
  }
20534
21966
  return row2;
20535
21967
  }
20536
- /** Handles dynamic tree controls without retaining detached nodes after a rerender. @param {Event} event @returns {void} */
21968
+ /** @param {Event} event @returns {void} */
20537
21969
  _onTreeClick(event) {
20538
21970
  const control = (
20539
21971
  /** @type {Element|null} */
@@ -20548,6 +21980,45 @@ var AppSidebar = class extends Component {
20548
21980
  if (item.children?.length) this.toggleBranch(item.id);
20549
21981
  else this._select(item, event);
20550
21982
  }
21983
+ /** @param {KeyboardEvent} event @returns {void} */
21984
+ _onTreeKeydown(event) {
21985
+ const current = (
21986
+ /** @type {Element|null} */
21987
+ event.target?.closest?.("[data-app-nav-id]")
21988
+ );
21989
+ if (!(current instanceof HTMLElement)) return;
21990
+ const item = findItem(this._items, current.getAttribute("data-app-nav-id"));
21991
+ const controls = visibleTreeControls(this.refs.list);
21992
+ const index = controls.indexOf(current);
21993
+ if (event.key === "ArrowRight" && item?.children?.length) {
21994
+ event.preventDefault();
21995
+ if (!this._expanded.has(String(item.id))) this.toggleBranch(item.id);
21996
+ else visibleTreeControls(current.parentElement?.querySelector(".zx-app-sidebar__sublist"))[0]?.focus();
21997
+ return;
21998
+ }
21999
+ if (event.key === "ArrowLeft") {
22000
+ if (item?.children?.length && this._expanded.has(String(item.id))) {
22001
+ event.preventDefault();
22002
+ this.toggleBranch(item.id);
22003
+ return;
22004
+ }
22005
+ const parentList = current.closest(".zx-app-sidebar__sublist");
22006
+ const parentControl = parentList?.parentElement?.querySelector(":scope > [data-app-nav-id]");
22007
+ if (parentControl instanceof HTMLElement) {
22008
+ event.preventDefault();
22009
+ parentControl.focus();
22010
+ }
22011
+ return;
22012
+ }
22013
+ let nextIndex;
22014
+ if (event.key === "Home") nextIndex = 0;
22015
+ else if (event.key === "End") nextIndex = controls.length - 1;
22016
+ else if (event.key === "ArrowDown") nextIndex = Math.min(controls.length - 1, index + 1);
22017
+ else if (event.key === "ArrowUp") nextIndex = Math.max(0, index - 1);
22018
+ else return;
22019
+ event.preventDefault();
22020
+ controls[nextIndex]?.focus();
22021
+ }
20551
22022
  /** @param {AppNavItem} item @param {Event} event @returns {void} */
20552
22023
  _select(item, event) {
20553
22024
  if (item.disabled) {
@@ -20569,14 +22040,14 @@ var AppSidebar = class extends Component {
20569
22040
  }
20570
22041
  /** @returns {void} */
20571
22042
  _syncActive() {
22043
+ if (!this.refs.list) return;
20572
22044
  const activeId = this._active == null ? null : String(this._active);
20573
22045
  for (const control of this.refs.list.querySelectorAll("[data-app-nav-id]")) {
20574
22046
  const item = findItem(this._items, control.getAttribute("data-app-nav-id"));
20575
22047
  const direct = activeId !== null && control.getAttribute("data-app-nav-id") === activeId;
20576
22048
  const descendant = activeId !== null && item ? containsItemId2(item.children ?? [], activeId) : false;
20577
- if (direct) {
20578
- control.setAttribute("aria-current", "page");
20579
- } else control.removeAttribute("aria-current");
22049
+ if (direct) control.setAttribute("aria-current", "page");
22050
+ else control.removeAttribute("aria-current");
20580
22051
  if (descendant) control.dataset.activeDescendant = "true";
20581
22052
  else delete control.dataset.activeDescendant;
20582
22053
  }
@@ -20590,7 +22061,7 @@ var AppSidebar = class extends Component {
20590
22061
  for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
20591
22062
  }
20592
22063
  };
20593
- function navControl2(item, disclosure, expanded, controls) {
22064
+ function navControl2(item, disclosure, expanded, controls, visual) {
20594
22065
  const tag = item.href && !disclosure ? "a" : "button";
20595
22066
  return (
20596
22067
  /** @type {HTMLElement} */
@@ -20606,7 +22077,7 @@ function navControl2(item, disclosure, expanded, controls) {
20606
22077
  ariaControls: controls,
20607
22078
  dataset: { appNavId: String(item.id) }
20608
22079
  },
20609
- item.icon ? h("span", { class: "zx-app-sidebar__item-icon", ariaHidden: "true" }, icon(item.icon)) : null,
22080
+ visual ? h("span", { class: "zx-app-sidebar__item-icon", ariaHidden: "true" }, visual) : null,
20610
22081
  h("span", { class: "zx-app-sidebar__item-label" }, item.label),
20611
22082
  item.badge != null ? h("span", { class: "zx-app-sidebar__item-badge" }, String(item.badge)) : null,
20612
22083
  disclosure ? h(
@@ -20617,6 +22088,13 @@ function navControl2(item, disclosure, expanded, controls) {
20617
22088
  )
20618
22089
  );
20619
22090
  }
22091
+ function visibleTreeControls(root) {
22092
+ if (!root) return [];
22093
+ return (
22094
+ /** @type {HTMLElement[]} */
22095
+ [...root.querySelectorAll("[data-app-nav-id]")].filter((control) => !control.closest(".zx-app-sidebar__sublist[hidden]"))
22096
+ );
22097
+ }
20620
22098
  function visibleTreeControl(control) {
20621
22099
  let candidate = (
20622
22100
  /** @type {HTMLElement|null} */
@@ -20649,12 +22127,17 @@ function containsItemId2(items, id) {
20649
22127
  }
20650
22128
  function appendSlot2(target, content) {
20651
22129
  if (content == null) return;
20652
- if (typeof content === "string" || typeof content === "number") target.append(document.createTextNode(String(content)));
20653
- else if (typeof content === "object" && typeof content.toElement === "function") {
22130
+ if (typeof content === "string" || typeof content === "number") {
22131
+ target.append(document.createTextNode(String(content)));
22132
+ } else if (typeof content === "object" && typeof content.toElement === "function") {
20654
22133
  const element = content.toElement();
20655
22134
  if (element) target.append(element);
20656
22135
  } else if (typeof content === "object" && typeof content.nodeType === "number") target.append(content);
20657
22136
  }
22137
+ function normalizeSide(orientation, side) {
22138
+ if (orientation === "horizontal") return side === "bottom" ? "bottom" : "top";
22139
+ return side === "right" ? "right" : "left";
22140
+ }
20658
22141
 
20659
22142
  // src/components/message/message.js
20660
22143
  var MessageQueue = class {
@@ -21991,7 +23474,7 @@ var Grid = class _Grid extends Table {
21991
23474
  * @param {GridOptions & import('../table/table.js').TableOptions} [options={}] Grid/Table options.
21992
23475
  */
21993
23476
  constructor(target = null, options = {}) {
21994
- super(target, options);
23477
+ super(target, { editTrigger: "single", ...options });
21995
23478
  }
21996
23479
  /**
21997
23480
  * Creates an editable hierarchical billing-line grid.
@@ -22022,14 +23505,17 @@ function billingItemsConfig(options = {}) {
22022
23505
  } = options;
22023
23506
  const fields = { ...DEFAULT_FIELDS, ...requestedFields };
22024
23507
  const calculateLineTotal = typeof lineTotal === "function" ? lineTotal : (row2) => {
22025
- const quantity = Number(row2?.[fields.quantity]);
22026
- const price = Number(row2?.[fields.unitPrice]);
22027
- return Number.isFinite(quantity) && Number.isFinite(price) ? quantity * price : null;
23508
+ const quantity = billingNumber(row2?.[fields.quantity]);
23509
+ const price = billingNumber(row2?.[fields.unitPrice]);
23510
+ return quantity !== null && price !== null ? quantity * price : null;
22028
23511
  };
22029
23512
  const columns = Array.isArray(suppliedColumns) ? suppliedColumns.map((column) => ({ ...column })) : billingColumns(fields, { units, currencies, currency, locale, decimals, columnOverrides });
22030
23513
  return {
22031
23514
  rowId: fields.id,
22032
23515
  editMode: "cell",
23516
+ editTrigger: "single",
23517
+ rowReorder: true,
23518
+ columnVisibility: true,
22033
23519
  hierarchy: { parentId: fields.parent, column: fields.item, expanded: true },
22034
23520
  responsive: "sm",
22035
23521
  ...tableOptions,
@@ -22129,6 +23615,11 @@ function billingColumns(fields, { units, currencies, currency, locale, decimals,
22129
23615
  function hasChoices(value) {
22130
23616
  return Array.isArray(value) ? value.length > 0 : Boolean(value && typeof value === "object" && Object.keys(value).length);
22131
23617
  }
23618
+ function billingNumber(value) {
23619
+ if (value == null || typeof value === "string" && value.trim() === "") return null;
23620
+ const number2 = Number(value);
23621
+ return Number.isFinite(number2) ? number2 : null;
23622
+ }
22132
23623
 
22133
23624
  // src/components/chart/chart.js
22134
23625
  var Chart = class extends Component {
@@ -22176,6 +23667,7 @@ var Chart = class extends Component {
22176
23667
  this._chartOptions = cloneChartValue(this.options.chartOptions);
22177
23668
  this._loading = Boolean(this.options.loading);
22178
23669
  this._error = null;
23670
+ this._hasError = false;
22179
23671
  this._handle = null;
22180
23672
  this._destroyed = false;
22181
23673
  const descriptionId = uid("zx-chart-description");
@@ -22221,6 +23713,7 @@ var Chart = class extends Component {
22221
23713
  setData(data) {
22222
23714
  this._data = normalizeChartData(data);
22223
23715
  this._error = null;
23716
+ this._hasError = false;
22224
23717
  this._loading = false;
22225
23718
  this._renderSummary();
22226
23719
  this._mountOrUpdate(true);
@@ -22254,8 +23747,9 @@ var Chart = class extends Component {
22254
23747
  return this;
22255
23748
  }
22256
23749
  /** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
22257
- setError(error) {
22258
- this._error = error ?? new Error(String(this.options.errorText));
23750
+ setError(error2) {
23751
+ this._error = error2 ?? new Error(String(this.options.errorText));
23752
+ this._hasError = true;
22259
23753
  this._loading = false;
22260
23754
  this._destroyHandle();
22261
23755
  this._syncState();
@@ -22265,6 +23759,7 @@ var Chart = class extends Component {
22265
23759
  /** Clears the error and remounts when data and an adapter are available. @returns {this} */
22266
23760
  clearError() {
22267
23761
  this._error = null;
23762
+ this._hasError = false;
22268
23763
  this._mountOrUpdate(false);
22269
23764
  return this;
22270
23765
  }
@@ -22272,8 +23767,8 @@ var Chart = class extends Component {
22272
23767
  resize() {
22273
23768
  try {
22274
23769
  this._handle?.resize?.();
22275
- } catch (error) {
22276
- this.setError(error);
23770
+ } catch (error2) {
23771
+ this.setError(error2);
22277
23772
  }
22278
23773
  return this;
22279
23774
  }
@@ -22303,7 +23798,7 @@ var Chart = class extends Component {
22303
23798
  /** @param {boolean} emitUpdate @returns {void} */
22304
23799
  _mountOrUpdate(emitUpdate) {
22305
23800
  this._syncState();
22306
- if (this._loading || this._error || isEmptyChartData(this._data)) {
23801
+ if (this._loading || this._hasError || isEmptyChartData(this._data)) {
22307
23802
  this._destroyHandle();
22308
23803
  return;
22309
23804
  }
@@ -22326,8 +23821,8 @@ var Chart = class extends Component {
22326
23821
  this.emit("ready", { instance: this.getInstance() });
22327
23822
  }
22328
23823
  if (emitUpdate) this.emit("update", { data: this._data, type: this._type });
22329
- } catch (error) {
22330
- this.setError(error);
23824
+ } catch (error2) {
23825
+ this.setError(error2);
22331
23826
  }
22332
23827
  }
22333
23828
  /** @returns {ChartSpec} */
@@ -22358,7 +23853,7 @@ var Chart = class extends Component {
22358
23853
  /** @returns {void} */
22359
23854
  _syncState() {
22360
23855
  const empty = isEmptyChartData(this._data);
22361
- const state = this._loading ? "loading" : this._error ? "error" : empty ? "empty" : "ready";
23856
+ const state = this._loading ? "loading" : this._hasError ? "error" : empty ? "empty" : "ready";
22362
23857
  this.el.dataset.state = state;
22363
23858
  this.refs.viewport.hidden = state !== "ready";
22364
23859
  let text4 = "";
@@ -22569,8 +24064,9 @@ function themeChartOptions(root) {
22569
24064
  borderColor: borderColor || void 0
22570
24065
  };
22571
24066
  }
22572
- function errorMessage(error, fallback) {
22573
- return error && typeof error === "object" && "message" in error ? String(error.message) : String(fallback);
24067
+ function errorMessage(error2, fallback) {
24068
+ const message = error2 && typeof error2 === "object" && "message" in error2 ? String(error2.message) : "";
24069
+ return message.trim() ? message : String(fallback);
22574
24070
  }
22575
24071
 
22576
24072
  // src/components/tree/hierarchy.js
@@ -23208,8 +24704,8 @@ var TreeView = class extends Component {
23208
24704
  node[this._childrenKey] = Array.isArray(children) ? children : [];
23209
24705
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
23210
24706
  return true;
23211
- } catch (error) {
23212
- this.emit("error", { error, node, id });
24707
+ } catch (error2) {
24708
+ this.emit("error", { error: error2, node, id });
23213
24709
  return false;
23214
24710
  } finally {
23215
24711
  this._loading.delete(id);
@@ -23697,9 +25193,9 @@ var Finder = class extends Component {
23697
25193
  const children = await this.options.load(node);
23698
25194
  node[this._childrenKey] = Array.isArray(children) ? children : [];
23699
25195
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
23700
- } catch (error) {
25196
+ } catch (error2) {
23701
25197
  node[this._childrenKey] = [];
23702
- this.emit("error", { error, node, id });
25198
+ this.emit("error", { error: error2, node, id });
23703
25199
  } finally {
23704
25200
  this._loading.delete(id);
23705
25201
  }
@@ -24129,6 +25625,821 @@ function restoreCustomElements(origins) {
24129
25625
  }
24130
25626
  }
24131
25627
 
25628
+ // src/components/filter/filter-model.js
25629
+ var filterOperators = Object.freeze([
25630
+ op("contains", "contains", "single", ["text"]),
25631
+ op("notContains", "does not contain", "single", ["text"]),
25632
+ op("eq", "is", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25633
+ op("neq", "is not", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25634
+ op("startsWith", "starts with", "single", ["text"]),
25635
+ op("endsWith", "ends with", "single", ["text"]),
25636
+ op("lt", "is less than", "single", ["number", "money"]),
25637
+ op("lte", "is at most", "single", ["number", "money"]),
25638
+ op("gt", "is greater than", "single", ["number", "money"]),
25639
+ op("gte", "is at least", "single", ["number", "money"]),
25640
+ op("on", "is on", "single", ["date", "datetime"]),
25641
+ op("notOn", "is not on", "single", ["date", "datetime"]),
25642
+ op("before", "is before", "single", ["date", "datetime"]),
25643
+ op("onOrBefore", "is on or before", "single", ["date", "datetime"]),
25644
+ op("after", "is after", "single", ["date", "datetime"]),
25645
+ op("onOrAfter", "is on or after", "single", ["date", "datetime"]),
25646
+ op("between", "is between", "pair", ["number", "money", "date", "datetime"]),
25647
+ op("notBetween", "is not between", "pair", ["number", "money", "date", "datetime"]),
25648
+ op("withinLast", "is within the last", "pair", ["date", "datetime"]),
25649
+ op("withinNext", "is within the next", "pair", ["date", "datetime"]),
25650
+ op("anyOf", "is any of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25651
+ op("noneOf", "is none of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25652
+ op("containsAny", "contains any", "many", ["tags"]),
25653
+ op("containsAll", "contains all", "many", ["tags"]),
25654
+ op("containsNone", "contains none", "many", ["tags"]),
25655
+ op("isEmpty", "is empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"]),
25656
+ op("isNotEmpty", "is not empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"])
25657
+ ]);
25658
+ function emptyFilterAst() {
25659
+ return { version: 1, root: { kind: "group", id: uid("filter-root"), logic: "and", children: [] } };
25660
+ }
25661
+ function filterCondition(initial = {}) {
25662
+ return {
25663
+ kind: "condition",
25664
+ id: String(initial.id || uid("filter-condition")),
25665
+ field: String(initial.field ?? ""),
25666
+ operator: String(initial.operator ?? ""),
25667
+ value: cloneFilterValue(initial.value ?? null)
25668
+ };
25669
+ }
25670
+ function cloneFilterValue(value) {
25671
+ return cloneJson(value);
25672
+ }
25673
+ function filterGroup(logic = "and", children = []) {
25674
+ return { kind: "group", id: uid("filter-group"), logic, children };
25675
+ }
25676
+ function parseFilterAst(value, limits = {}) {
25677
+ const maxDepth = finiteLimit(limits.maxDepth, 3);
25678
+ const maxConditions = finiteLimit(limits.maxConditions, 50);
25679
+ if (!plainObject(value) || value.version !== 1 || !plainObject(value.root)) {
25680
+ throw new TypeError("Filter AST must have version 1 and a root group");
25681
+ }
25682
+ const ids = /* @__PURE__ */ new Set();
25683
+ let conditions = 0;
25684
+ const visit = (raw2, depth, root2 = false) => {
25685
+ if (!plainObject(raw2)) throw new TypeError("Filter nodes must be plain objects");
25686
+ const id = String(raw2.id ?? "").trim();
25687
+ if (!id || ids.has(id)) throw new TypeError("Filter node IDs must be non-empty and unique");
25688
+ ids.add(id);
25689
+ if (raw2.kind === "group") {
25690
+ if (depth > maxDepth) throw new RangeError("Filter group depth exceeds maxDepth");
25691
+ if (raw2.logic !== "and" && raw2.logic !== "or") throw new TypeError("Filter group logic must be and or or");
25692
+ if (!Array.isArray(raw2.children)) throw new TypeError("Filter group children must be an array");
25693
+ return {
25694
+ kind: "group",
25695
+ id,
25696
+ logic: raw2.logic,
25697
+ children: raw2.children.map((child) => visit(child, depth + 1, false))
25698
+ };
25699
+ }
25700
+ if (root2 || raw2.kind !== "condition") throw new TypeError("Filter root must be a group");
25701
+ conditions += 1;
25702
+ if (conditions > maxConditions) throw new RangeError("Filter condition count exceeds maxConditions");
25703
+ return {
25704
+ kind: "condition",
25705
+ id,
25706
+ field: String(raw2.field ?? ""),
25707
+ operator: String(raw2.operator ?? ""),
25708
+ value: cloneJson(raw2.value)
25709
+ };
25710
+ };
25711
+ const root = visit(value.root, 0, true);
25712
+ if (root.kind !== "group") throw new TypeError("Filter root must be a group");
25713
+ return (
25714
+ /** @type {FilterAst} */
25715
+ { version: 1, root }
25716
+ );
25717
+ }
25718
+ function cloneFilterAst(value) {
25719
+ return parseFilterAst(value, { maxDepth: Infinity, maxConditions: Infinity });
25720
+ }
25721
+ function stringifyFilterAst(value) {
25722
+ return JSON.stringify(cloneFilterAst(value));
25723
+ }
25724
+ function validateFilterAst(ast, fields, operators = filterOperators) {
25725
+ const errors = [];
25726
+ const fieldMap = new Map((Array.isArray(fields) ? fields : []).map((field) => [String(field.id), field]));
25727
+ const operatorMap = new Map((Array.isArray(operators) ? operators : []).map((operator) => [String(operator.id), operator]));
25728
+ const visit = (node, root = false) => {
25729
+ if (node.kind === "group") {
25730
+ if (!root && node.children.length === 0) errors.push(error(node.id, "empty-group", "Nested groups need at least one condition"));
25731
+ node.children.forEach((child) => visit(child));
25732
+ return;
25733
+ }
25734
+ const field = fieldMap.get(node.field);
25735
+ if (!field) errors.push(error(node.id, "unknown-field", node.field ? `Unavailable field: ${node.field}` : "Choose a field"));
25736
+ const operator = operatorMap.get(node.operator);
25737
+ if (!operator) {
25738
+ errors.push(error(node.id, "unknown-operator", node.operator ? `Unavailable operator: ${node.operator}` : "Choose an operator"));
25739
+ return;
25740
+ }
25741
+ if (field) {
25742
+ const allowed = Array.isArray(field.operators) && field.operators.length ? field.operators : operators.filter((candidate) => candidate.types.includes(field.type)).map((candidate) => candidate.id);
25743
+ if (!allowed.includes(operator.id) || !operator.types.includes(field.type)) {
25744
+ errors.push(error(node.id, "operator-type", `${operator.label} is not available for ${field.label}`));
25745
+ }
25746
+ }
25747
+ if (!validArity(operator.arity, node.value)) {
25748
+ 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"));
25749
+ }
25750
+ };
25751
+ visit(ast.root, true);
25752
+ return { valid: errors.length === 0, errors };
25753
+ }
25754
+ function op(id, label, arity, types) {
25755
+ return Object.freeze({ id, label, arity, types: Object.freeze(types) });
25756
+ }
25757
+ function error(nodeId, code, message) {
25758
+ return { nodeId, code, message };
25759
+ }
25760
+ function validArity(arity, value) {
25761
+ if (arity === "none") return value === null;
25762
+ if (arity === "pair") return Array.isArray(value) && value.length === 2 && value.every(filled);
25763
+ if (arity === "many") return Array.isArray(value) && value.length > 0 && value.every(filled);
25764
+ return filled(value);
25765
+ }
25766
+ function filled(value) {
25767
+ return value !== null && value !== void 0 && !(typeof value === "string" && value.trim() === "");
25768
+ }
25769
+ function cloneJson(value, seen = /* @__PURE__ */ new WeakSet()) {
25770
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
25771
+ if (typeof value === "number") {
25772
+ if (!Number.isFinite(value)) throw new TypeError("Filter values cannot contain non-finite numbers");
25773
+ return value;
25774
+ }
25775
+ if (typeof value !== "object" || value instanceof Date || typeof value.nodeType === "number") {
25776
+ throw new TypeError("Filter values must be JSON-safe");
25777
+ }
25778
+ if (seen.has(value)) throw new TypeError("Filter values cannot contain cycles");
25779
+ seen.add(value);
25780
+ if (Array.isArray(value)) {
25781
+ const result2 = value.map((item) => cloneJson(item, seen));
25782
+ seen.delete(value);
25783
+ return result2;
25784
+ }
25785
+ if (!plainObject(value)) throw new TypeError("Filter values must be plain JSON objects");
25786
+ const result = {};
25787
+ for (const [key, item] of Object.entries(value)) {
25788
+ Object.defineProperty(result, key, {
25789
+ value: cloneJson(item, seen),
25790
+ enumerable: true,
25791
+ configurable: true,
25792
+ writable: true
25793
+ });
25794
+ }
25795
+ seen.delete(value);
25796
+ return result;
25797
+ }
25798
+ function plainObject(value) {
25799
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
25800
+ const prototype = Object.getPrototypeOf(value);
25801
+ return prototype === Object.prototype || prototype === null;
25802
+ }
25803
+ function finiteLimit(value, fallback) {
25804
+ if (value === Infinity) return Infinity;
25805
+ const number2 = Number(value);
25806
+ return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : fallback;
25807
+ }
25808
+
25809
+ // src/components/filter/filter.js
25810
+ var Filter = class extends Component {
25811
+ static cssName = "filter";
25812
+ /** @type {Readonly<FilterOptions>} */
25813
+ static defaults = {
25814
+ fields: [],
25815
+ value: null,
25816
+ operators: [],
25817
+ allowGroups: true,
25818
+ maxDepth: 3,
25819
+ maxConditions: 50,
25820
+ autoApply: false,
25821
+ readonly: false,
25822
+ disabled: false,
25823
+ applyLabel: "Apply filters",
25824
+ clearLabel: "Clear"
25825
+ };
25826
+ /** @returns {HTMLElement} */
25827
+ render() {
25828
+ this._createdRoot = this.el === null;
25829
+ const root = (
25830
+ /** @type {HTMLElement} */
25831
+ this.el ?? h("div")
25832
+ );
25833
+ this.el = root;
25834
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
25835
+ this._fields = (Array.isArray(this.options.fields) ? this.options.fields : []).map((field) => ({ ...field }));
25836
+ this._operators = mergeOperators(filterOperators, this.options.operators);
25837
+ this._readonly = Boolean(this.options.readonly);
25838
+ this._disabled = Boolean(this.options.disabled);
25839
+ this._async = /* @__PURE__ */ new Map();
25840
+ this._customEditors = [];
25841
+ this._value = this.options.value == null ? emptyFilterAst() : parseFilterAst(this.options.value, this._limits());
25842
+ this.listen(root, "change", (event) => this._handleChange(event));
25843
+ this.listen(root, "input", (event) => this._handleInput(event));
25844
+ this.listen(root, "click", (event) => this._handleClick(event));
25845
+ this._render();
25846
+ return root;
25847
+ }
25848
+ /** Returns a defensive AST copy. @returns {FilterAst} */
25849
+ getValue() {
25850
+ return cloneFilterAst(this._value);
25851
+ }
25852
+ /** Atomically replaces the AST. @param {unknown} value @param {{silent?:boolean}} [options={}] @returns {this} */
25853
+ setValue(value, { silent = false } = {}) {
25854
+ const parsed = parseFilterAst(value, this._limits());
25855
+ this._abortAll();
25856
+ this._value = parsed;
25857
+ this._render();
25858
+ if (!silent) this._changed("set", this._value.root.id);
25859
+ return this;
25860
+ }
25861
+ /** Adds a condition to a group. @param {string|null} [parentId=null] @param {Record<string,unknown>} [initial={}] @returns {string|null} */
25862
+ addCondition(parentId = null, initial = {}) {
25863
+ if (!this._canMutate() || conditionCount(this._value.root) >= Number(this.options.maxConditions)) return null;
25864
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
25865
+ if (!parent) return null;
25866
+ const first = this._fields[0];
25867
+ const field = String(initial.field ?? first?.id ?? "");
25868
+ const definition = this._field(field);
25869
+ const node = filterCondition({
25870
+ ...initial,
25871
+ field,
25872
+ operator: String(initial.operator ?? this._defaultOperator(definition) ?? "")
25873
+ });
25874
+ parent.children.push(node);
25875
+ this._render();
25876
+ this._changed("add", node.id);
25877
+ this.focus(node.id);
25878
+ return node.id;
25879
+ }
25880
+ /** Adds a nested group. @param {string|null} [parentId=null] @param {'and'|'or'} [logic='and'] @returns {string|null} */
25881
+ addGroup(parentId = null, logic = "and") {
25882
+ if (!this._canMutate() || !this.options.allowGroups) return null;
25883
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
25884
+ if (!parent || groupDepth(this._value.root, parent.id) >= Number(this.options.maxDepth)) return null;
25885
+ const group = filterGroup(logic === "or" ? "or" : "and");
25886
+ parent.children.push(group);
25887
+ this._render();
25888
+ this._changed("add", group.id);
25889
+ this.focus(group.id);
25890
+ return group.id;
25891
+ }
25892
+ /** Updates a node by stable ID. @param {string} id @param {Record<string,unknown>} patch @returns {this} */
25893
+ update(id, patch = {}) {
25894
+ if (!this._canMutate()) return this;
25895
+ const node = findNode2(this._value.root, id);
25896
+ if (!node) return this;
25897
+ if (node.kind === "group") {
25898
+ if (patch.logic === "and" || patch.logic === "or") node.logic = patch.logic;
25899
+ } else {
25900
+ if (Object.hasOwn(patch, "field")) node.field = String(patch.field ?? "");
25901
+ if (Object.hasOwn(patch, "operator")) node.operator = String(patch.operator ?? "");
25902
+ if (Object.hasOwn(patch, "value")) node.value = parseValueJson(patch.value);
25903
+ }
25904
+ this._abort(id);
25905
+ this._render();
25906
+ this._changed("update", id);
25907
+ return this;
25908
+ }
25909
+ /** Removes a non-root node. @param {string} id @returns {this} */
25910
+ remove(id) {
25911
+ if (!this._canMutate() || id === this._value.root.id) return this;
25912
+ const parent = findParent(this._value.root, id);
25913
+ if (!parent) return this;
25914
+ const index = parent.children.findIndex((child) => child.id === id);
25915
+ const focusId = parent.children[index + 1]?.id ?? parent.children[index - 1]?.id ?? parent.id;
25916
+ const removed = removeNode(this._value.root, id);
25917
+ if (!removed) return this;
25918
+ this._abortTree(removed);
25919
+ this._render();
25920
+ const label = removed.kind === "condition" ? this._field(removed.field)?.label ?? "Filter" : "Filter group";
25921
+ this._announce(`${label} removed`);
25922
+ this._changed("remove", id);
25923
+ this.focus(focusId);
25924
+ return this;
25925
+ }
25926
+ /** Moves a node into a group while preserving its identity. @param {string} id @param {string} parentId @param {number} index @returns {this} */
25927
+ move(id, parentId, index) {
25928
+ const node = findNode2(this._value.root, id);
25929
+ const parent = findGroup(this._value.root, parentId);
25930
+ const previous = findParent(this._value.root, id);
25931
+ if (!this._canMutate() || id === this._value.root.id || !node || !parent || !previous || containsNode(node, parentId)) return this;
25932
+ const previousIndex = previous.children.findIndex((child) => child.id === id);
25933
+ const removed = previous.children.splice(previousIndex, 1)[0];
25934
+ parent.children.splice(Math.max(0, Math.min(parent.children.length, Number(index) || 0)), 0, removed);
25935
+ try {
25936
+ parseFilterAst(this._value, this._limits());
25937
+ } catch {
25938
+ parent.children.splice(parent.children.indexOf(removed), 1);
25939
+ previous.children.splice(previousIndex, 0, removed);
25940
+ return this;
25941
+ }
25942
+ this._render();
25943
+ this._changed("move", id);
25944
+ return this;
25945
+ }
25946
+ /** Returns semantic validity and errors. @returns {{valid:boolean,errors:Array<Record<string,string>>}} */
25947
+ validate() {
25948
+ return validateFilterAst(this._value, this._fields, this._operators);
25949
+ }
25950
+ /** Applies only a valid AST. @returns {FilterAst|null} */
25951
+ apply() {
25952
+ if (this._disabled) return null;
25953
+ const result = this.validate();
25954
+ if (!result.valid) {
25955
+ this.emit("invalid", { errors: result.errors });
25956
+ this.focus(result.errors[0]?.nodeId);
25957
+ return null;
25958
+ }
25959
+ const value = this.getValue();
25960
+ this.emit("apply", { value });
25961
+ return value;
25962
+ }
25963
+ /** Clears all conditions. @param {{silent?:boolean}} [options={}] @returns {this} */
25964
+ clear({ silent = false } = {}) {
25965
+ if (!this._canMutate()) return this;
25966
+ this._abortAll();
25967
+ this._value.root.children = [];
25968
+ this._render();
25969
+ if (!silent) this._changed("clear", this._value.root.id);
25970
+ this.refs.add?.focus();
25971
+ return this;
25972
+ }
25973
+ /** Focuses a condition's field or the Add button. @param {string|null} [id=null] @returns {this} */
25974
+ focus(id = null) {
25975
+ const selector = id ? `[data-node-id="${cssEscape(id)}"] :is([data-filter-field], [data-filter-logic], [data-filter-action="add"])` : '[data-filter-action="add"]';
25976
+ queueMicrotask(() => (
25977
+ /** @type {HTMLElement|null} */
25978
+ this.el.querySelector(selector)?.focus()
25979
+ ));
25980
+ return this;
25981
+ }
25982
+ /** @param {boolean} readonly @returns {this} */
25983
+ setReadonly(readonly) {
25984
+ this._readonly = Boolean(readonly);
25985
+ this._render();
25986
+ return this;
25987
+ }
25988
+ /** @returns {this} */
25989
+ enable() {
25990
+ this._disabled = false;
25991
+ this._render();
25992
+ return this;
25993
+ }
25994
+ /** @returns {this} */
25995
+ disable() {
25996
+ this._disabled = true;
25997
+ this._abortAll();
25998
+ this._render();
25999
+ return this;
26000
+ }
26001
+ /** @returns {void} */
26002
+ destroy() {
26003
+ this._abortAll();
26004
+ this._destroyEditors();
26005
+ const root = this.el;
26006
+ const snapshot10 = this._snapshot;
26007
+ super.destroy();
26008
+ if (!this._createdRoot && root) restoreTarget(root, snapshot10);
26009
+ }
26010
+ /** @returns {void} */
26011
+ _render() {
26012
+ this._destroyEditors();
26013
+ this._renderVersion = (this._renderVersion ?? 0) + 1;
26014
+ const status = h("div", { ref: "status", class: "zx-filter__status", role: "status", ariaLive: "polite" });
26015
+ const tree = this._renderGroup(this._value.root, 0, true);
26016
+ const actions2 = h(
26017
+ "div",
26018
+ { class: "zx-filter__actions" },
26019
+ actionButton("add", this._value.root.id, "plus", "Add filter", { ref: "add", disabled: this._blocked() }),
26020
+ this.options.allowGroups ? actionButton("add-group", this._value.root.id, "folder", "Add group", { disabled: this._blocked() }) : null,
26021
+ actionButton("clear", "", "x", String(this.options.clearLabel), { disabled: this._blocked() }),
26022
+ actionButton("apply", "", "check", String(this.options.applyLabel), { class: "zx-button--primary", disabled: this._disabled })
26023
+ );
26024
+ this.el.replaceChildren(status, tree, actions2);
26025
+ this.refs.status = status;
26026
+ this.refs.add = /** @type {HTMLElement} */
26027
+ this.el.querySelector('[data-filter-action="add"]');
26028
+ this.el.toggleAttribute("data-readonly", this._readonly);
26029
+ this.el.toggleAttribute("data-disabled", this._disabled);
26030
+ const result = this.validate();
26031
+ this.el.dataset.valid = String(result.valid);
26032
+ for (const error2 of result.errors) this.el.querySelector(`[data-node-id="${cssEscape(error2.nodeId)}"]`)?.setAttribute("data-invalid", "");
26033
+ }
26034
+ /** @param {any} group @param {number} depth @param {boolean} root @returns {HTMLElement} */
26035
+ _renderGroup(group, depth, root) {
26036
+ const children = h(
26037
+ "ol",
26038
+ { class: "zx-filter__children" },
26039
+ group.children.map((node) => node.kind === "group" ? this._renderGroup(node, depth + 1, false) : this._renderCondition(node))
26040
+ );
26041
+ const logic = h(
26042
+ "select",
26043
+ {
26044
+ class: "zx-filter__logic",
26045
+ dataset: { filterLogic: "", nodeId: group.id },
26046
+ ariaLabel: root ? "Root filter logic" : "Group filter logic",
26047
+ disabled: this._blocked()
26048
+ },
26049
+ h("option", { value: "and", selected: group.logic === "and" }, "Match all"),
26050
+ h("option", { value: "or", selected: group.logic === "or" }, "Match any")
26051
+ );
26052
+ const legend = h("legend", { class: "zx-filter__legend" }, logic, h("span", {}, root ? "conditions" : "in this group"));
26053
+ const controls = h(
26054
+ "div",
26055
+ { class: "zx-filter__group-actions" },
26056
+ actionButton("add", group.id, "plus", "Add filter", { disabled: this._blocked() }),
26057
+ this.options.allowGroups && depth < Number(this.options.maxDepth) ? actionButton("add-group", group.id, "folder", "Add subgroup", { disabled: this._blocked() }) : null,
26058
+ root ? null : actionButton("remove", group.id, "trash", "Remove group", { disabled: this._blocked() })
26059
+ );
26060
+ return h(root ? "fieldset" : "li", {
26061
+ class: "zx-filter__group",
26062
+ dataset: { nodeId: group.id, depth: String(depth) }
26063
+ }, root ? legend : h("fieldset", {}, legend, children, controls), root ? children : null, root ? controls : null);
26064
+ }
26065
+ /** @param {any} node @returns {HTMLElement} */
26066
+ _renderCondition(node) {
26067
+ const field = this._field(node.field);
26068
+ const operators = this._operatorsFor(field);
26069
+ const operator = this._operator(node.operator);
26070
+ const fieldSelect = h(
26071
+ "select",
26072
+ {
26073
+ class: "zx-filter__field",
26074
+ dataset: { filterField: "", nodeId: node.id },
26075
+ ariaLabel: "Filter field",
26076
+ disabled: this._blocked()
26077
+ },
26078
+ h("option", { value: "", selected: !node.field }, "Choose a field\u2026"),
26079
+ this._fields.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.field }, candidate.label)),
26080
+ node.field && !field ? h("option", { value: node.field, selected: true }, `Unavailable: ${node.field}`) : null
26081
+ );
26082
+ const operatorSelect = h(
26083
+ "select",
26084
+ {
26085
+ class: "zx-filter__operator",
26086
+ dataset: { filterOperator: "", nodeId: node.id },
26087
+ ariaLabel: "Filter operator",
26088
+ disabled: this._blocked()
26089
+ },
26090
+ h("option", { value: "", selected: !node.operator }, "Choose an operator\u2026"),
26091
+ operators.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.operator }, candidate.label)),
26092
+ node.operator && !operators.some((candidate) => candidate.id === node.operator) ? h("option", { value: node.operator, selected: true }, `Unavailable: ${node.operator}`) : null
26093
+ );
26094
+ return h(
26095
+ "li",
26096
+ { class: "zx-filter__condition", dataset: { nodeId: node.id } },
26097
+ fieldSelect,
26098
+ operatorSelect,
26099
+ this._renderValue(node, field, operator),
26100
+ actionButton("remove", node.id, "trash", `Remove ${field?.label ?? "filter"}`, { disabled: this._blocked() })
26101
+ );
26102
+ }
26103
+ /** @param {any} node @param {FilterField|null} field @param {any} operator @returns {HTMLElement} */
26104
+ _renderValue(node, field, operator) {
26105
+ const host = h("div", { class: "zx-filter__value" });
26106
+ if (!field || !operator) return host;
26107
+ if (operator.arity === "none") return h("div", { class: "zx-filter__value zx-filter__value--empty" }, "No value");
26108
+ if (field.type === "custom" && typeof field.editor === "function") {
26109
+ const renderVersion = this._renderVersion;
26110
+ queueMicrotask(() => {
26111
+ if (renderVersion !== this._renderVersion || !this.el.contains(host)) return;
26112
+ const handle = field.editor(host, {
26113
+ value: node.value,
26114
+ field,
26115
+ conditionId: node.id,
26116
+ setValue: (value) => this._setNodeValue(node.id, parseValueJson(value))
26117
+ });
26118
+ if (handle && typeof handle.destroy === "function") this._customEditors.push(handle);
26119
+ });
26120
+ return host;
26121
+ }
26122
+ const values = operator.arity === "pair" ? Array.isArray(node.value) ? node.value : [null, null] : [node.value];
26123
+ if (operator.arity === "many") {
26124
+ const control = choiceSelect(field, node, values[0], true, this._blocked());
26125
+ if (control) return h("div", { class: "zx-filter__value" }, control);
26126
+ return h("div", { class: "zx-filter__value" }, valueInput(field, node.id, Array.isArray(node.value) ? node.value.join(", ") : "", 0, this._blocked(), true));
26127
+ }
26128
+ const controls = values.map((value, index) => choiceSelect(field, node, value, false, this._blocked(), index) ?? valueInput(field, node.id, value, index, this._blocked()));
26129
+ return h("div", { class: "zx-filter__value", dataset: { arity: operator.arity } }, controls);
26130
+ }
26131
+ /** @param {Event} event @returns {void} */
26132
+ _handleChange(event) {
26133
+ if (!this._canMutate()) return;
26134
+ const target = (
26135
+ /** @type {HTMLInputElement|HTMLSelectElement|null} */
26136
+ event.target
26137
+ );
26138
+ const id = target?.dataset.nodeId;
26139
+ if (!target || !id) return;
26140
+ if (target.dataset.filterField !== void 0) {
26141
+ const node = findNode2(this._value.root, id);
26142
+ if (node?.kind !== "condition") return;
26143
+ node.field = target.value;
26144
+ const field = this._field(node.field);
26145
+ node.operator = this._defaultOperator(field) ?? "";
26146
+ node.value = null;
26147
+ this._abort(id);
26148
+ this._render();
26149
+ this._changed("update", id);
26150
+ this.focus(id);
26151
+ } else if (target.dataset.filterOperator !== void 0) {
26152
+ const node = findNode2(this._value.root, id);
26153
+ if (node?.kind !== "condition") return;
26154
+ node.operator = target.value;
26155
+ node.value = null;
26156
+ this._abort(id);
26157
+ this._render();
26158
+ this._changed("update", id);
26159
+ queueMicrotask(() => (
26160
+ /** @type {HTMLElement|null} */
26161
+ this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`)?.focus()
26162
+ ));
26163
+ } else if (target.dataset.filterLogic !== void 0) {
26164
+ const node = findNode2(this._value.root, id);
26165
+ if (node?.kind === "group") node.logic = target.value === "or" ? "or" : "and";
26166
+ this._changed("update", id);
26167
+ } else if (target.dataset.filterValue !== void 0) this._valueInput(target);
26168
+ }
26169
+ /** @param {Event} event @returns {void} */
26170
+ _handleInput(event) {
26171
+ if (!this._canMutate()) return;
26172
+ const target = (
26173
+ /** @type {HTMLInputElement|null} */
26174
+ event.target
26175
+ );
26176
+ if (!target || target.dataset.filterValue === void 0) return;
26177
+ this._valueInput(target);
26178
+ const id = target.dataset.nodeId;
26179
+ const node = findNode2(this._value.root, id);
26180
+ const field = node?.kind === "condition" ? this._field(node.field) : null;
26181
+ if (field?.loadChoices) this._scheduleChoices(node.id, field, target.value);
26182
+ }
26183
+ /** @param {HTMLInputElement|HTMLSelectElement} target @returns {void} */
26184
+ _valueInput(target) {
26185
+ const node = findNode2(this._value.root, target.dataset.nodeId);
26186
+ if (node?.kind !== "condition") return;
26187
+ const operator = this._operator(node.operator);
26188
+ const field = this._field(node.field);
26189
+ if (!operator || !field) return;
26190
+ let value;
26191
+ if (target instanceof HTMLSelectElement && target.multiple) {
26192
+ value = Array.from(target.selectedOptions, (option) => parseScalar(option.value, field.type));
26193
+ } else if (operator.arity === "many") {
26194
+ value = target.value.split(",").map((item) => item.trim()).filter(Boolean).map((item) => parseScalar(item, field.type));
26195
+ } else if (operator.arity === "pair") {
26196
+ const pair = Array.isArray(node.value) ? [...node.value] : [null, null];
26197
+ pair[Number(target.dataset.valueIndex) || 0] = parseScalar(target.value, field.type);
26198
+ value = pair;
26199
+ } else value = parseScalar(target.value, field.type);
26200
+ this._setNodeValue(
26201
+ node.id,
26202
+ /** @type {any} */
26203
+ value
26204
+ );
26205
+ }
26206
+ /** @param {MouseEvent} event @returns {void} */
26207
+ _handleClick(event) {
26208
+ const button2 = (
26209
+ /** @type {HTMLElement|null} */
26210
+ event.target?.closest?.("[data-filter-action]")
26211
+ );
26212
+ if (!button2 || !this.el.contains(button2)) return;
26213
+ const action = button2.dataset.filterAction;
26214
+ const id = button2.dataset.nodeId || null;
26215
+ if (action === "add") this.addCondition(id);
26216
+ else if (action === "add-group") this.addGroup(id);
26217
+ else if (action === "remove" && id) this.remove(id);
26218
+ else if (action === "clear") this.clear();
26219
+ else if (action === "apply") this.apply();
26220
+ }
26221
+ /** @param {string} id @param {any} value @returns {void} */
26222
+ _setNodeValue(id, value) {
26223
+ const node = findNode2(this._value.root, id);
26224
+ if (node?.kind !== "condition") return;
26225
+ node.value = value;
26226
+ const result = this.validate();
26227
+ this.el.dataset.valid = String(result.valid);
26228
+ const row2 = this.el.querySelector(`[data-node-id="${cssEscape(id)}"]`);
26229
+ row2?.toggleAttribute("data-invalid", result.errors.some((candidate) => candidate.nodeId === id));
26230
+ this._changed("update", id);
26231
+ }
26232
+ /** @param {string} reason @param {string} nodeId @returns {void} */
26233
+ _changed(reason, nodeId) {
26234
+ const result = this.validate();
26235
+ const value = this.getValue();
26236
+ this.emit("change", { value, valid: result.valid, errors: result.errors, reason, nodeId });
26237
+ if (this.options.autoApply && result.valid) this.emit("apply", { value });
26238
+ }
26239
+ /** @param {string} id @param {FilterField} field @param {string} query @returns {void} */
26240
+ _scheduleChoices(id, field, query) {
26241
+ this._abort(id);
26242
+ if (query.length < Number(field.minQuery ?? 0)) return;
26243
+ const controller = new AbortController();
26244
+ this._setAsyncBusy(id, true);
26245
+ const entry = { controller, timer: setTimeout(async () => {
26246
+ this.emit("query", { conditionId: id, fieldId: field.id, query });
26247
+ try {
26248
+ const choices = await field.loadChoices({ query, signal: controller.signal, conditionId: id, field });
26249
+ if (controller.signal.aborted || this._async.get(id)?.controller !== controller) return;
26250
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26251
+ const listId = input?.getAttribute("list");
26252
+ const list = listId ? this.el.querySelector(`#${cssEscape(listId)}`) : null;
26253
+ if (list) list.replaceChildren(...(Array.isArray(choices) ? choices : []).map((choice) => h("option", { value: choice.value }, choice.label)));
26254
+ this.emit("loaded", { conditionId: id, fieldId: field.id, query, choices });
26255
+ } catch (error2) {
26256
+ if (!controller.signal.aborted) this.emit("error", { conditionId: id, fieldId: field.id, query, error: error2 instanceof Error ? error2 : new Error(String(error2)) });
26257
+ } finally {
26258
+ if (this._async.get(id)?.controller === controller) {
26259
+ this._async.delete(id);
26260
+ this._setAsyncBusy(id, false);
26261
+ }
26262
+ }
26263
+ }, Math.max(0, Number(field.debounce ?? 200))) };
26264
+ this._async.set(id, entry);
26265
+ }
26266
+ /** @param {string} id @returns {void} */
26267
+ _abort(id) {
26268
+ const entry = this._async.get(id);
26269
+ if (entry) {
26270
+ clearTimeout(entry.timer);
26271
+ entry.controller.abort();
26272
+ this._async.delete(id);
26273
+ this._setAsyncBusy(id, false);
26274
+ }
26275
+ }
26276
+ /** @param {string} id @param {boolean} busy @returns {void} */
26277
+ _setAsyncBusy(id, busy) {
26278
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26279
+ if (busy) input?.setAttribute("aria-busy", "true");
26280
+ else input?.removeAttribute("aria-busy");
26281
+ }
26282
+ /** @param {any} node @returns {void} */
26283
+ _abortTree(node) {
26284
+ if (node.kind === "condition") this._abort(node.id);
26285
+ else node.children.forEach((child) => this._abortTree(child));
26286
+ }
26287
+ /** @returns {void} */
26288
+ _abortAll() {
26289
+ for (const id of [...this._async.keys()]) this._abort(id);
26290
+ }
26291
+ /** @returns {void} */
26292
+ _destroyEditors() {
26293
+ for (const editor of this._customEditors ?? []) editor.destroy?.();
26294
+ this._customEditors = [];
26295
+ }
26296
+ /** @returns {boolean} */
26297
+ _blocked() {
26298
+ return this._readonly || this._disabled;
26299
+ }
26300
+ /** @returns {boolean} */
26301
+ _canMutate() {
26302
+ return !this._blocked();
26303
+ }
26304
+ /** @returns {{maxDepth:number,maxConditions:number}} */
26305
+ _limits() {
26306
+ return { maxDepth: Number(this.options.maxDepth), maxConditions: Number(this.options.maxConditions) };
26307
+ }
26308
+ /** @param {string} id @returns {FilterField|null} */
26309
+ _field(id) {
26310
+ return this._fields.find((field) => field.id === id) ?? null;
26311
+ }
26312
+ /** @param {string} id @returns {any} */
26313
+ _operator(id) {
26314
+ return this._operators.find((operator) => operator.id === id) ?? null;
26315
+ }
26316
+ /** @param {FilterField|null} field @returns {any[]} */
26317
+ _operatorsFor(field) {
26318
+ if (!field) return [];
26319
+ const ids = field.operators?.length ? field.operators : null;
26320
+ return this._operators.filter((operator) => ids ? ids.includes(operator.id) : operator.types.includes(field.type));
26321
+ }
26322
+ /** @param {FilterField|null} field @returns {string|null} */
26323
+ _defaultOperator(field) {
26324
+ return field?.defaultOperator && this._operatorsFor(field).some((operator) => operator.id === field.defaultOperator) ? field.defaultOperator : this._operatorsFor(field)[0]?.id ?? null;
26325
+ }
26326
+ /** @param {string} text @returns {void} */
26327
+ _announce(text4) {
26328
+ if (this.refs.status) this.refs.status.textContent = text4;
26329
+ }
26330
+ };
26331
+ function actionButton(action, id, iconName, label, props = {}) {
26332
+ 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));
26333
+ }
26334
+ function choiceSelect(field, node, value, multiple, disabled, index = 0) {
26335
+ const choices = Array.isArray(field.choices) && field.choices.length ? field.choices : field.type === "boolean" ? [{ value: true, label: "True" }, { value: false, label: "False" }] : null;
26336
+ if (!choices) return null;
26337
+ const values = new Set(Array.isArray(value) ? value.map(String) : [String(value ?? "")]);
26338
+ return (
26339
+ /** @type {HTMLSelectElement} */
26340
+ h(
26341
+ "select",
26342
+ {
26343
+ dataset: { filterValue: "", nodeId: node.id, valueIndex: String(index) },
26344
+ ariaLabel: `${field.label} value`,
26345
+ multiple,
26346
+ disabled
26347
+ },
26348
+ !multiple ? h("option", { value: "", selected: !values.has(String(value)) || value == null }, "Choose\u2026") : null,
26349
+ choices.map((choice) => h("option", { value: choice.value, selected: values.has(String(choice.value)) }, choice.label))
26350
+ )
26351
+ );
26352
+ }
26353
+ function valueInput(field, id, value, index, disabled, many = false) {
26354
+ const type = many ? "text" : ["number", "money"].includes(field.type) ? "number" : field.type === "date" ? "date" : field.type === "datetime" ? "datetime-local" : "text";
26355
+ const listId = field.loadChoices ? `zx-filter-choices-${id}-${index}` : null;
26356
+ const input = (
26357
+ /** @type {HTMLInputElement} */
26358
+ h("input", {
26359
+ type,
26360
+ value: value ?? "",
26361
+ dataset: { filterValue: "", nodeId: id, valueIndex: String(index) },
26362
+ ariaLabel: `${field.label} value${index ? ` ${index + 1}` : ""}`,
26363
+ disabled,
26364
+ placeholder: many ? "Comma-separated values" : "",
26365
+ list: listId
26366
+ })
26367
+ );
26368
+ return h(
26369
+ "span",
26370
+ { class: "zx-filter__value-control" },
26371
+ input,
26372
+ listId ? h("datalist", { id: listId }) : null
26373
+ );
26374
+ }
26375
+ function parseScalar(value, type) {
26376
+ if (value === "") return null;
26377
+ if (["number", "money"].includes(type)) {
26378
+ const number2 = Number(value);
26379
+ return Number.isFinite(number2) ? number2 : null;
26380
+ }
26381
+ if (type === "boolean") return value === "true";
26382
+ return value;
26383
+ }
26384
+ function parseValueJson(value) {
26385
+ return cloneFilterValue(value === void 0 ? null : value);
26386
+ }
26387
+ function findNode2(root, id) {
26388
+ if (!root || root.id === id) return root ?? null;
26389
+ if (root.kind === "group") for (const child of root.children) {
26390
+ const match = findNode2(child, id);
26391
+ if (match) return match;
26392
+ }
26393
+ return null;
26394
+ }
26395
+ function findGroup(root, id) {
26396
+ const node = findNode2(root, id);
26397
+ return node?.kind === "group" ? node : null;
26398
+ }
26399
+ function findParent(root, id) {
26400
+ if (root?.kind !== "group") return null;
26401
+ if (root.children.some((child) => child.id === id)) return root;
26402
+ for (const child of root.children) {
26403
+ const parent = findParent(child, id);
26404
+ if (parent) return parent;
26405
+ }
26406
+ return null;
26407
+ }
26408
+ function removeNode(root, id) {
26409
+ if (root.kind !== "group") return null;
26410
+ const index = root.children.findIndex((child) => child.id === id);
26411
+ if (index >= 0) return root.children.splice(index, 1)[0];
26412
+ for (const child of root.children) {
26413
+ const found = removeNode(child, id);
26414
+ if (found) return found;
26415
+ }
26416
+ return null;
26417
+ }
26418
+ function containsNode(node, id) {
26419
+ return Boolean(node && findNode2(node, id));
26420
+ }
26421
+ function conditionCount(node) {
26422
+ return node.kind === "condition" ? 1 : node.children.reduce((sum, child) => sum + conditionCount(child), 0);
26423
+ }
26424
+ function groupDepth(root, id, depth = 0) {
26425
+ if (root.id === id) return depth;
26426
+ if (root.kind === "group") for (const child of root.children) {
26427
+ if (child.kind === "group") {
26428
+ const found = groupDepth(child, id, depth + 1);
26429
+ if (found >= 0) return found;
26430
+ }
26431
+ }
26432
+ return -1;
26433
+ }
26434
+ function mergeOperators(base, extra) {
26435
+ const map = new Map(base.map((item) => [item.id, { ...item, types: [...item.types] }]));
26436
+ 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] : [] });
26437
+ return [...map.values()];
26438
+ }
26439
+ function cssEscape(value) {
26440
+ return globalThis.CSS?.escape ? CSS.escape(value) : String(value).replaceAll(/[^a-zA-Z0-9_-]/g, "\\$&");
26441
+ }
26442
+
24132
26443
  // src/components/fieldset/fieldset.js
24133
26444
  var Fieldset = class extends Component {
24134
26445
  static cssName = "fieldset";
@@ -25296,8 +27607,8 @@ var Questionnaire = class extends Component {
25296
27607
  this._setBusy(true);
25297
27608
  try {
25298
27609
  message = await item.validate(answer, this.getAnswers());
25299
- } catch (error) {
25300
- message = error instanceof Error ? error.message : String(error);
27610
+ } catch (error2) {
27611
+ message = error2 instanceof Error ? error2.message : String(error2);
25301
27612
  } finally {
25302
27613
  if (!this._destroyed) this._setBusy(false);
25303
27614
  }
@@ -25435,10 +27746,10 @@ var Questionnaire = class extends Component {
25435
27746
  }
25436
27747
  /** @param {NormalizedItem} item @returns {void} */
25437
27748
  _paintError(item) {
25438
- const error = this._itemHost.querySelector(".zx-questionnaire__error");
25439
- if (error instanceof HTMLElement) {
25440
- error.textContent = this._error;
25441
- error.hidden = !this._error;
27749
+ const error2 = this._itemHost.querySelector(".zx-questionnaire__error");
27750
+ if (error2 instanceof HTMLElement) {
27751
+ error2.textContent = this._error;
27752
+ error2.hidden = !this._error;
25442
27753
  }
25443
27754
  if (this._error) this._itemHost.setAttribute("aria-invalid", "true");
25444
27755
  else this._itemHost.removeAttribute("aria-invalid");
@@ -25446,7 +27757,7 @@ var Questionnaire = class extends Component {
25446
27757
  if (field) {
25447
27758
  if (this._error) field.setHighlight(this._error, "danger");
25448
27759
  else field.clearHighlight();
25449
- if (error instanceof HTMLElement) error.hidden = true;
27760
+ if (error2 instanceof HTMLElement) error2.hidden = true;
25450
27761
  }
25451
27762
  }
25452
27763
  /** @returns {void} */
@@ -25669,10 +27980,11 @@ function registerFieldAdapters() {
25669
27980
  registerFieldAdapters();
25670
27981
  export {
25671
27982
  AccountMenu,
25672
- AppRail,
27983
+ AppIcon,
25673
27984
  AppSidebar,
25674
27985
  Avatar,
25675
27986
  Breadcrumb,
27987
+ Card,
25676
27988
  Chart,
25677
27989
  ChartJsAdapter,
25678
27990
  CheckButton,
@@ -25692,6 +28004,7 @@ export {
25692
28004
  Field,
25693
28005
  FieldUpload,
25694
28006
  Fieldset,
28007
+ Filter,
25695
28008
  Finder,
25696
28009
  Form,
25697
28010
  Grid,
@@ -25732,6 +28045,7 @@ export {
25732
28045
  __,
25733
28046
  addDays,
25734
28047
  addMonths,
28048
+ appIcon,
25735
28049
  appendCompact,
25736
28050
  appendCompactChild,
25737
28051
  appendContent,
@@ -25751,6 +28065,7 @@ export {
25751
28065
  clampRange,
25752
28066
  clampSize,
25753
28067
  cloneChartValue,
28068
+ cloneFilterAst,
25754
28069
  compactElement,
25755
28070
  configureIcons,
25756
28071
  copyButton,
@@ -25761,12 +28076,16 @@ export {
25761
28076
  defineElements,
25762
28077
  describe,
25763
28078
  downloadBlob,
28079
+ emptyFilterAst,
25764
28080
  emptyState,
25765
28081
  escapeRegExp,
25766
28082
  faFamilies,
25767
28083
  faIconClasses,
25768
28084
  faNames,
25769
28085
  faStyles,
28086
+ filterCondition,
28087
+ filterGroup,
28088
+ filterOperators,
25770
28089
  fireCompactEvent,
25771
28090
  focusTrap,
25772
28091
  formatCurrency,
@@ -25806,6 +28125,7 @@ export {
25806
28125
  paginationRange,
25807
28126
  parseCompactTag,
25808
28127
  parseDate,
28128
+ parseFilterAst,
25809
28129
  parseIconSpec,
25810
28130
  parseNumber,
25811
28131
  parseRangeText,
@@ -25817,6 +28137,7 @@ export {
25817
28137
  rankLauncherItems,
25818
28138
  registerFieldAdapters,
25819
28139
  registerIcons,
28140
+ reorderTableRows,
25820
28141
  resolveElement,
25821
28142
  resolveSize,
25822
28143
  rovingTabindex,
@@ -25833,6 +28154,7 @@ export {
25833
28154
  stack,
25834
28155
  stepPrecision,
25835
28156
  storage,
28157
+ stringifyFilterAst,
25836
28158
  throttle,
25837
28159
  toArray,
25838
28160
  toCsv,
@@ -25844,5 +28166,6 @@ export {
25844
28166
  uniqueBy,
25845
28167
  useBuiltinIcons,
25846
28168
  useFontAwesome,
28169
+ validateFilterAst,
25847
28170
  zeyosService
25848
28171
  };